Documentation · Built-in Commerce

Built-In Commerce Engine Integration

Connect an embedded Presenter3D experience to Presenter3D-managed product prices, stock, markets, and checkout. This guide covers the browser code added to a merchant page, including the supplied checkout interface and buyer-account access.

Requires
Commerce Engine access
Credential
Browser integration token
Checkout
Supplied UI or custom
Updated
On this page
  1. Required Integration Values
  2. Register Before Loading Commerce Data
  3. Product Price, Quantity, and Checkout
    1. Page Markup
    2. Browser Setup
    3. What the Display Represents
    4. Product Quantity
  4. Markets
  5. Availability and Failure States
  6. Presenter3D Checkout UI
  7. Buyer Login and Buyer Portal
  8. Custom Checkout UI
  9. Flat Displays and Product Images
  10. Native Commerce Method Options
    1. Checkout Portal Options
    2. Viewer-Based Helpers
  11. Test and Deploy
    1. Notes for AI Coding Agents

Start with Embedding Presenter3D and Connecting Your UI for the viewer, hybrid images, external controls, language selection, shared configurations, access restrictions, and loading states. Those features do not require native commerce registration.

Use this guide only when Presenter3D is the commerce service for the integration. If prices, stock, and checkout belong to your existing store, keep that system responsible for them; do not register a native commerce token merely to use the viewer or gallery.

Reflection is not registration: reflectBusinessData() can display an already available viewer summary without a token. Registration is needed to obtain authorized native business data independently of 3D and use native checkout, not merely to format that existing summary. See summary reflection options for token-free display binding.

Required Integration Values

A Commerce Engine subscription is required for native commerce. Obtain the token and configured business-context values from the Presenter3D dashboard. If you are integrating on a customer's behalf without dashboard access, ask the customer to provide these values; do not request their dashboard credentials.

Value Purpose
Published project ID Identifies the configured product; obtain it from the project's Integration section.
Commerce API base URL https://server.presenter3d.com. This is the backend address, not the merchant's website or the viewer/loader URL. Do not append /mcp or /api/commerce; the runtime adds the appropriate request paths.
Browser integration token Generate it for the selected business context and approved merchant origin under the dashboard's Bookkeeping API access section in organization settings. This is the browser token used by api.register().
Approved host origin The merchant page's scheme, hostname, and port, configured for the business context and token.
Market labels Use the business context's configured market labels, or automatic resolution where configured.
Environment Select the development or production business context and its corresponding token and payment configuration.

The account must have active Commerce Engine access and the product must be configured for the selected business context. The dashboard provides the subscribed commerce settings subject to account permissions. Shipping, tax, payment-provider settings, branding, and account administration are separate setup topics; storefront code does not configure those services.

Use only the token intended for browser integration. Never place provider secret keys, webhook secrets, dashboard credentials, or server administration tokens in merchant JavaScript. Project access codes and buyer login/session tokens are different credentials and are not substitutes for commerce registration.

Register Before Loading Commerce Data

Use the minimal hybrid integration as the base for this example. Keep its markup, loader, and single <script type="module">. Inside that module's try block, insert the registration below immediately after const api = window.presenter3d.commerce;, before api.registerVariations(...) and before starting the viewer. The base already declares viewer, projectId, api, and bindings; do not redeclare them or add a second setup script.

JavaScript
api.register({
  apiBase: "https://server.presenter3d.com",
  token: "YOUR_BROWSER_COMMERCE_TOKEN",
  host: window.location.origin,
  market: "YOUR_MARKET_LABEL",
  paymentProvider: "stripe",
});

Registration is page-level and synchronous. It does not confirm that the token is valid; authorized requests do that. One context can serve several projects on the page. Register once at application bootstrap rather than independently in every product component.

api.register(options, scopeOptions?) Type / behavior
options.token Required browser integration token.
options.apiBase "https://server.presenter3d.com"; specify it for initial registration. No /mcp suffix.
options.host Merchant origin; normally window.location.origin.
options.market Configured market label or "auto".
options.fallbackMarket Concrete fallback market for automatic resolution.
options.paymentProvider "stripe" or "fake"; use only the provider configured for the environment. fake is for testing.
options.createNonce Optional () => string request-nonce factory; normally leave unset.
scopeOptions.surfaceScope "all" (default) or "commerce-driven"; the latter uses targets registered with registerCommerceDrivenSurfaces().

The string shorthand api.register(token) does not supply an API base. Use the full object for an initial integration. Generate a suitable token for each approved origin/context. Switching a business context between development and production requires a newly issued matching token.

On a fresh page, the embedding guide's wait/setup/start sequence provides an integration point. On an SPA where the runtime is already active, register the intended context before mounting/starting its commerce viewers. Registering after a viewer's initial request does not retroactively authorize or reorder that request.

The runtime attaches the required authorization and merchant-origin information to its requests. Do not manually duplicate its HTTP calls or set a fake host origin to bypass restrictions.

Product Price, Quantity, and Checkout

Page Markup

Place these controls alongside the viewer from the embedding guide. The checkout button below is merchant-owned, not an externalUIManager control, so its initial disabled state is intentional.

HTML
<h2 id="product-name">Product</h2>
<p>
  <span id="base-price"></span>
  <span id="effective-price">Price unavailable</span>
  <span id="discount"></span>
</p>
<p id="stock"></p>
<p id="delivery"></p>

<label for="product-quantity">Quantity</label>
<button id="quantity-minus" type="button" aria-label="Decrease quantity">-</button>
<input id="product-quantity" type="number" min="1" step="1" value="1">
<button id="quantity-plus" type="button" aria-label="Increase quantity">+</button>

<button id="buy-now" type="button" disabled>Buy now</button>
<p id="commerce-status" role="status"></p>

Browser Setup

Assemble the example with these exact edits to the minimal hybrid integration:

  1. Add the commerce markup above before its setup module. Keep the existing viewer, finish controls, loading status, and loader.
  2. Insert api.register(...) at the location described above, once.
  3. Inside the existing try block, replace only await viewer.start(); with the entire block below. Keep the media/control/progress bindings before it and the existing catch and pagehide cleanup after it.

This is one module, not concatenated independent scripts. The replacement reuses viewer, projectId, api, and bindings, installs commerce controls, and starts 3D once without making checkout wait for it. Its handles join the existing cleanup list. These insertion points refer to the minimal hybrid example, not the separate external-tray recipe. Replace example IDs only if your markup uses different IDs.

JavaScript
const message = document.getElementById("commerce-status");
const buyButton = document.getElementById("buy-now");

const prices = api.reflectBusinessData({
  projectId,
  basePriceElementId: "base-price",
  basePriceConfig: { crossedOut: true, showOnlyOnDiscount: true },
  effectivePriceElementId: "effective-price",
  discountElementId: "discount",
  stockElementId: "stock",
  deliveryElementId: "delivery",
});
const quantity = api.bindProductQuantityInput({
  projectId,
  elementId: "product-quantity",
  decrementElementId: "quantity-minus",
  incrementElementId: "quantity-plus",
});
bindings.push(prices, quantity);

let checkoutPending = false;
const openCheckout = async () => {
  if (checkoutPending) return;
  checkoutPending = true;
  buyButton.disabled = true;
  message.textContent = "Opening checkout...";
  try {
    const result = await api.openCheckoutPortal({ projectId });
    message.textContent = result.status === "completed"
      ? "Your order has been completed."
      : "Checkout closed.";
  } catch (error) {
    message.textContent = error instanceof Error
      ? error.message
      : "Checkout is unavailable. Please try again.";
  } finally {
    checkoutPending = false;
    if (buyButton.isConnected) buyButton.disabled = false;
  }
};
buyButton.addEventListener("click", openCheckout);
bindings.push({ dispose() { buyButton.removeEventListener("click", openCheckout); } });
buyButton.disabled = false;

// Register any hybrid media and viewer progress bindings before this point.
// Start 3D independently; checkout does not wait for it to become ready.
if (viewer.getAttribute("flat") === null || viewer.getAttribute("flat") === "false") {
  void viewer.start().catch(error => {
    console.error("The 3D view could not start", error);
    // The embedding guide's progress/failure binding owns the viewer notice.
  });
}

try {
  const response = await api.getProductOverview({ projectId });
  if (response.overview?.productName) {
    document.getElementById("product-name").textContent = response.overview.productName;
  }
} catch (error) {
  // Do not overwrite a valid price already delivered by a later business summary.
  console.error("Initial product overview could not be loaded", error);
  if (!checkoutPending && !message.textContent) {
    message.textContent = "Initial product details could not be loaded.";
  }
}

The initial price placeholder is not a spinner. A failed request must not leave an impression that prices are still loading indefinitely. Later valid summaries can replace the placeholder. Provide an appropriate retry or contact path in your page's error presentation.

The existing pagehide cleanup disposes the added price/quantity bindings and checkout listener through bindings. For an SPA, run equivalent cleanup in the component's unmount hook and prevent late asynchronous results from updating a replaced route's DOM. Do not place two checkout click handlers on the same button or open a second checkout while the first promise is pending.

What the Display Represents

getProductOverview({ projectId }) retrieves lightweight initial display data. The response may include overview.productName, overview.unitAmountMinor, overview.currency, and market labels. It is not the final price of an arbitrary configured product and is not payment authority.

reflectBusinessData() keeps the supplied DOM fields synchronized with the current business summary, variation, quantity, market, and language. It formats amounts and labels; do not assume every currency has two decimal places or manually divide every minor-unit value by 100. Leaving locale unset follows the runtime language and then browser locale. Set locale explicitly only when you intentionally want that formatting override.

This binding itself does not require native registration when the viewer already provides a summary. Its availability and authority are those of the source data. Without independent authorized business loading, a failed or not-yet-ready viewer may have no summary to reflect. The binding does not fetch or upgrade it. See reflection options for optional text prefixes/suffixes, currency placement, strikethrough, discount-only display and the legacy amount-element alias.

The reflection binding uses plain text, not merchant-supplied HTML. You can supply just the element IDs your page needs. refresh(summary?) rerenders its current/supplied summary; dispose() removes its listeners.

The viewer also exposes getBusinessDataSummary() when its API is available and emits businessDataSummary updates. For ordinary price panels, prefer the reflection binding rather than reproducing pricing rules. On multi-product pages, always filter events by project; early summaries can contain only initial display data.

Product Quantity

Quantity is stored per project and shared across its surfaces:

JavaScript
api.setProductQuantity({ projectId, quantity: 2 });
const selectedQuantity = api.getProductQuantity(projectId);

This is the number of configured products being purchased, not the number of shelves, handles, or other components inside one configuration. Reflected totals combine selected product quantity with the configured line quantities. Discount rules can differ per line, so do not calculate all prices from one displayed discount percentage.

bindProductQuantityInput() defaults to a minimum of 1 and accepts min, max, and onChange. A page-side maximum is a UI constraint, not a stock reservation. openCheckoutPortal() uses the stored product quantity unless an explicit quantity is supplied. The server validates the actual price and availability again.

Markets

Market values are configured labels, not arbitrary country codes. Use the business context's dashboard labels or those returned by the API. Pages should not mix conflicting commerce-market values across tags that share one page-level context.

JavaScript
await api.setMarket("YOUR_MARKET_LABEL");
await api.setMarket("auto");
await api.setMarket(null); // Clear the explicit market.

auto requests resolution using visitor location and configured regional rules; it is not itself a real market. Registration can include market: "auto" and a concrete fallbackMarket. Do not invent a country-to-market mapping on the client. Where applicable, bindMarketSelect() can populate valid market choices:

HTML
<label for="shop-market">Market</label>
<select id="shop-market"><option value="">Select market</option></select>
JavaScript
const markets = api.bindMarketSelect({
  elementId: "shop-market",
  populate: true,
  includeAuto: true,
  persist: "session",
});

This binding performs the normal market change and can remember the choice for the session. It does not automatically opt into configuration preservation. For a custom selector that must preserve a configured product, call:

JavaScript
const shared = await api.setMarket("YOUR_DESTINATION_MARKET", {
  preserveConfiguration: true,
  projectId,
});

This requires a connected, ready, idle, server-backed 3D viewer, a matching surface, and a concrete destination market; auto is not accepted for preservation. The configuration is saved before the market is changed. On failure, do not present the destination as successfully selected. Use a custom handler for this behavior, not a second handler on a select already bound by bindMarketSelect().

The selected project's surfaces receive its shared hash; other projects follow their normal market-change restoration policy. The promise is not a guarantee that 3D restoration has finished. The receiving market validates the setup using current eligibility rules. Out-of-stock status affects orderability, not restoration; removed or incompatible choices can reject the whole saved setup.

If decorative or language-only experiences share the page, register the intended targets using registerCommerceDrivenSurfaces([{ projectId, instanceId }]) and pass surfaceScope: "commerce-driven" to scoped registration/market methods. Omit instanceId to target every surface of that project. Default scope remains all surfaces.

Language changes are independent and are documented in Languages. Checkout can use that selected language; changing language does not change the market. A standalone ?view=...&market=... URL is not a substitute for an authorized commerce integration.

Availability and Failure States

Situation Merchant-page behavior
Overview is pending or fails Keep a nonmisleading placeholder or last valid display value; offer retry/contact UI as appropriate. Never invent a zero price.
Viewer lacks WebGL2 Keep registered images/gallery usable and show the viewer warning. Authorized native commerce may still load product data without 3D.
Product or requested quantity is out of stock Display the returned availability. Checkout review/reservation determines whether it can be ordered.
Saved configuration contains removed/disabled/incompatible choices Reject the whole saved setup through the existing restoration flow; do not silently present a partial restoration as complete.
Token/context/origin is invalid Explain that commerce is unavailable and check the integration values. A viewer can still render without authorized commerce data.
Commerce Engine access is inactive Handle checkout rejection and its unavailable message; do not leave a loading state active.
Checkout is cancelled Restore your page controls. Cancellation is not successful order completion.
Payment/finalization fails or is unresolved Show the returned failure/state. Do not claim success, create a replacement payment blindly, or calculate a final total yourself.

Display prices, stock, and delivery dates are previews. Shipping, tax, discounts, stock, and final totals are revalidated through server checkout. The gallery's current image is not evidence that a product is orderable.

The WebGL fallback can use the renderer-independent business path when native commerce is registered. It still requires valid authorization, active access, and permitted networking. A deferred commerce-network policy can postpone that work; it must not be misreported as a device limitation.

Presenter3D Checkout UI

The recommended entry point is:

JavaScript
const result = await api.openCheckoutPortal({
  projectId,
  // Omit quantity to use the current bound product quantity.
});

It opens Presenter3D's checkout interface and coordinates the product review, buyer details, reservation, payment, and order-finalization flow. It can prepare product state before 3D is ready. Checkout code loads on demand; external controls do not need to manage its modules or payment-provider SDK.

The resolved result has status: "completed" or status: "cancelled", a provider, and optional review/reservation/payment/order details. Errors can reject the promise. The example above uses the returned status; alternatively use onComplete and onCancel callbacks. Do not run the same order-completion side effect twice through both the callback and returned result.

Common options are projectId, quantity, and those callbacks. locale can override the checkout UI language; otherwise the runtime considers buyer details, the selected experience language, and the document language. Most pages should keep the shared language selection rather than add a separate checkout-language control.

Closing checkout returns control to the page. Do not assume that closing a browser window proves a payment was cancelled: server/order state remains authoritative. Keep payment and finalization behavior in the supplied flow unless you are intentionally implementing the custom-checkout contract.

Buyer Login and Buyer Portal

The supplied checkout UI supports buyer login using an emailed one-time code, saved buyer details, and a My Orders entry. That entry obtains a server-authorized buyer-profile link. The buyer portal is separate from the checkout dialog and from the merchant dashboard.

For an account link in merchant-owned UI, the checkout helper provides the same underlying operations. There is no global api.openBuyerPortal() shortcut. Obtain a helper after commerce registration:

JavaScript
const checkout = await api.getCheckout({ projectId });

When the visitor submits an email address, request a code:

JavaScript
const challenge = await checkout.requestBuyerLoginCode({ email: submittedEmail });
if (!challenge.challengeToken) throw new Error("A login challenge was not returned.");

After the visitor enters the code received by email, verify it and request a link:

JavaScript
const login = await checkout.verifyBuyerLoginCode({
  challengeToken: challenge.challengeToken,
  code: submittedCode,
});
if (!login.buyerSessionToken) throw new Error("Buyer login did not complete.");

const profile = await checkout.createBuyerProfileLink({
  buyerSessionToken: login.buyerSessionToken,
});
if (!profile.profileUrl) throw new Error("The buyer portal link is unavailable.");

const accountLink = document.getElementById("buyer-account-link");
accountLink.href = profile.profileUrl;
accountLink.hidden = false;

The link element can be:

HTML
<a id="buyer-account-link" hidden target="_blank" rel="noopener noreferrer">My orders</a>

These are separate user actions, not a script that guesses or auto-submits a login code. Add busy states and try/catch around each handler; handle expired/invalid challenges and a missing buyer account using the server response. Using a link after it is ready avoids relying on a popup opened after an asynchronous request.

Keep challenge/session tokens and buyer URLs out of logs, analytics, public share URLs, and project configuration records. Do not construct a portal URL yourself or assume an arbitrary merchant-owned domain is supported. The helper also offers loadBuyerSessionProfile() and updateBuyerCheckoutProfile() for custom saved-details UI; the supplied checkout already coordinates its own login/details experience.

Checkout helper method Arguments / promise result
requestBuyerLoginCode({ email, locale? }) Email required; optional UI locale. Returns a challenge including challengeToken.
verifyBuyerLoginCode({ challengeToken, code }) Both strings required. Returns profile/session response, including buyerSessionToken when provided.
loadBuyerSessionProfile({ buyerSessionToken }) Returns the authorized buyer profile/session response.
createBuyerProfileLink({ buyerSessionToken, returnToShopUrl?, returnToCheckoutUrl? }) Returns profileUrl; optional return URLs remain subject to server validation.
updateBuyerCheckoutProfile({ buyerSessionToken, buyerDetails }) Returns the updated profile/session response. buyerDetails fields are listed below.

Custom Checkout UI

Use the lower-level helper only when your page will own the checkout screens. This is not a different commerce service: it uses the same server review, reservation, and payment operations as the supplied UI.

Step Helper method Responsibility
Obtain current configuration buildReservationConfig({ quantity }) Use the configured product and slot identities; do not rebuild them from UI labels.
Review reviewCheckout(...) Display server-calculated totals for the actual buyer/delivery inputs.
Reserve createReservation(...) Retain the returned reservation state/token and honor its expiry.
Set up payment createStripePaymentIntent(...) Use the active reservation and server-returned payment setup.
Confirm provider payment Provider UI/SDK The helper's payment-intent creation alone does not confirm a card payment.
Finalize order confirmStripePayment(...) Complete the server-side order flow using the same reservation and actual payment intent.
Abandon a reservation cancelReservation(...) Release an unused reservation through the supported server path.

This sequence is a responsibility map, not a complete payment implementation. Custom checkout must also handle address changes, revised totals, authentication, reservation/payment-window expiry, cancellations, pending finalization, and recovery after network loss. The detailed checkout-helper reference is not included on this page. Before implementing custom checkout, contact Presenter3D to request the reference for your integration, providing the project ID and intended checkout flow, but no tokens or credentials. Prefer openCheckoutPortal() when your page does not need to own those responsibilities; its setup and options are documented above.

Do not treat getCheckout() as harmless code prefetch: it also prepares product data. Each call creates a helper; retain the appropriate helper for your custom checkout flow rather than starting independent concurrent flows.

Flat Displays and Product Images

Project-backed flat surfaces can show variations and participate in shared selection and native commerce without starting 3D. A hybrid viewer can also start checkout while its 3D content is still loading. These cases do not require manufacturing a fake 3D-ready event.

A permanent flat surface is a presentation choice, not an API for creating arbitrary catalog products. For products without an underlying 3D project, confirm the available flat-product commerce setup with Presenter3D before integrating; this guide does not define a standalone catalog-import API.

Registered productImageUrl or variantImageUrl can provide the client-side checkout illustration. That does not automatically persist the URL into server-generated order emails or invoices. Authored configuration snapshots and server-side image handling are separate from the gallery. Flat-only surfaces also cannot generate a 3D snapshot or shared 3D configuration simply because checkout is available.

Native Commerce Method Options

Use the browser integration token from the dashboard and https://server.presenter3d.com as the API base. These are native-commerce methods; general viewer and existing-summary display methods remain token-free.

Method Options / return
register(optionsOrToken, scopeOptions?) Synchronous. Full object: required token; optional apiBase, host, market, fallbackMarket, paymentProvider, createNonce. Scope object: surfaceScope. Use full initial registration, real API base/origin; string shorthand only supplies token.
setMarket(market?, options?) Promise; concrete label, auto, or null to clear. Options: surfaceScope, preserveConfiguration, projectId. Normal result undefined; preservation returns shared record and requires concrete market, not auto/null.
bindMarketSelect(options) Required elementId; optional populate, includeAuto, persist, exclude, labels, surfaceScope. Returns { refresh(), dispose() }; normal switching, no preserve option.
setProductQuantity({ projectId?, quantity }) Set positive product count, not component count. getProductQuantity(projectId?) reads it.
bindProductQuantityInput(options) Required elementId; optional projectId, incrementElementId, decrementElementId, min (1), max, onChange(quantity). Returns { refresh(), dispose() }.
getProductOverview(options) Required projectId; optional market, regionGroupId, registration, refresh. Promise of display overview/market metadata. refresh: true requests fresh overview; not a configuration/checkout review.
getCheckout(options?) Optional projectId, registration overrides, checkout: { fetchImpl?, recordCheckoutAction? }. Promise of a newly prepared helper, not singleton/prefetch. Prefer supplied portal for standard checkout.
openCheckoutPortal(options?) Promise with completed/cancelled status or rejection; options below. Does not require ready 3D.
attachAddressAutocomplete(options) Required DOM container and onAddressSelected({ address, formattedAddress?, place? }); optional projectId, browser apiKey, placeholder, className, includedRegionCodes: string[], onError(error). Resolves to { element, focus(), destroy() } or undefined when unavailable; call destroy() on teardown.

Market-select defaults: includeAuto true, populate false, persistence session. persist: "session" or true remembers a choice; "none" or false does not. exclude filters configured labels, labels maps actual values to display labels. Do not substitute labels for real market IDs or add a second preservation handler to an already-bound select.

JavaScript
const markets = api.bindMarketSelect({
  elementId: "market", populate: true, includeAuto: true, persist: "session",
  exclude: ["OWNER_SUPPLIED_EXCLUDED_MARKET"],
  labels: { auto: "Choose automatically" }, surfaceScope: "commerce-driven",
});
const quantity = api.bindProductQuantityInput({
  projectId, elementId: "quantity", decrementElementId: "minus", incrementElementId: "plus",
  min: 1, max: 10, onChange: value => { quantityLabel.textContent = String(value); },
});
const overview = await api.getProductOverview({ projectId, market: "OWNER_SUPPLIED_MARKET", refresh: true });

Checkout Portal Options

All api.openCheckoutPortal(options?) fields are optional. Supply projectId explicitly on multi-product pages.

Option Type / behavior
projectId Published project ID; otherwise uses the runtime's selected/default project.
quantity Product count; defaults to the stored quantity for that project.
locale, projectName Checkout language and display-name overrides. Omitted locale follows the runtime's language resolution.
initialBuyerDetails Initial buyer/address values, using the fields below. These are not proof of identity.
mount DOM element for the portal; otherwise its normal document-level mount.
paymentProvider, provider Configured "stripe" or "fake"; provider takes precedence, then paymentProvider, then registration, then "stripe".
buyerSessionToken Server-issued buyer session credential, not the browser integration token.
providerMeta Provider-specific metadata object; not a place for secret keys or client-calculated payment authority.
snapshotLayoutHash Authored checkout snapshot-layout identifier.
selectedRateId Shipping-rate identifier returned by checkout review; do not fabricate one.
onStatus ({ phase, message?, review?, reservation?, paymentIntent?, order?, error? }) => void | Promise<void>.
onComplete (result) => void | Promise<void>; same completion result as the promise.
onCancel () => void | Promise<void>.
maxFinalizationAttempts Number of finalization attempts; default 6, minimum 1.
captureSnapshotLayoutHash () => string | undefined | Promise<string | undefined> override.
capturePreviewImage () => Blob | Blob[] | string | undefined or a promise of that result.
uploadSnapshots (orderId, snapshotLayoutHash?, snapshots?) => unknown | Promise<unknown> override; snapshots are Blob, Blob[] or string.

Leave capture/upload callbacks unset to retain runtime behavior. onStatus.phase is one of opened, reviewing, reviewed, cancelled, reserving, reserved, payment_intent, awaiting_payment, confirming, completed, snapshot_uploading, snapshot_uploaded, snapshot_failed. A status notification alone is not payment authority; use the final result and server order state.

initialBuyerDetails accepts these optional fields:

Fields Type
email, phone, cardholderName, firstName, lastName, fullName, company, taxRegistrationNumber, locale Strings.
buyerType "individual" or "business".
sameAsBilling, saveToBuyerProfile Booleans.
clientBuyerToken Existing client buyer identifier, not a substitute for an authenticated buyer session.
billingAddress, shippingAddress Objects with optional string fields: label, recipientName, company, line1, line2, city, state, postalCode, countryCode, phone.
JavaScript
const result = await api.openCheckoutPortal({
  projectId, quantity: 2, locale: "fr",
  onStatus: ({ phase, message }) => { checkoutStatus.textContent = message || phase; },
});
// Handle the result once. Do not duplicate order handling in onComplete as well.

Only use supported configured providers. Browser apiKey fields are not a place for provider secret keys. Native amounts and availability are revalidated by checkout. The supplied portal already handles its own buyer login/address/payment screens.

Viewer-Based Helpers

Prefer the global methods above for new native-commerce integrations. The following instance methods depend on a loaded viewer and its business configuration; check that the method exists before calling it.

Method Arguments / result
viewer.buildBusinessProductConfig({ quantity? }) Synchronously builds { regionGroupId?, productConfig }. The product config includes projectId, quantity and configuration lines; do not reconstruct it from labels.
viewer.commerce.getCheckout(registrationOverrides?) Instance shortcut with optional registration fields directly, not the global { projectId, registration, checkout } wrapper. Returns a new checkout helper.
viewer.subscribeToBusinessNotifications(options) Required token, email; optional apiBase, host, merchantCustomerId, scope: "product" | "item" (default item), quantity, productConfig, item, items, expectation: "expected" | "unknown" | "backorder". Returns { ok, message?, subscriberId?, subscriptionId?, scope?, itemCount?, unsubscribeToken? }.
viewer.unsubscribeFromBusinessNotifications({ token, apiBase?, item? }) Here token is the returned unsubscribe token. Optional item is { type, id }. Returns { ok, message?, subscriberId?, canceledSubscriptions?, canceledItems? }.

Notification items use { type, id, sku?, expectation? }; type is "object", "material", "color" or "texture". id is the business item ID, not its UI label. Legacy article is accepted as an input fallback for sku. Subscribe only after the visitor requests notifications; protect returned unsubscribe credentials. Product config and item references must come from the runtime or supplied business setup.

Test and Deploy

  1. Use a supplied development context and its matching origin/token/payment setup. Do not test by placing unintended real orders.
  2. Verify initial overview, changed variations, quantity, discounts, stock, and delivery previews. Confirm checkout reviews fresh server totals.
  3. Test configured market labels, automatic resolution, unavailable markets, and preservation failures. Do not bind two independent handlers to one market selector.
  4. Test checkout completion, cancellation, repeated clicks, network failure, and buyer-login errors. Keep successful order handling idempotent on your page.
  5. Test with 3D still loading and with WebGL2 unavailable; keep the flat layer usable and show appropriate viewer and commerce messages separately.
  6. Test narrow layouts, fullscreen exit, SPA teardown/remount, language reload, and multiple product surfaces.
  7. Verify production origin/context/token and active service configuration before release. A rendering viewer alone does not prove commerce authorization.
  8. Apply the site's CSP to the supplied API, media, checkout, and payment-provider origins. Never expose provider secrets or log buyer credentials.
  9. Dispose merchant bindings/listeners on teardown without disposing the page-wide provider used by other products.

Notes for AI Coding Agents

  • Use https://server.presenter3d.com and the customer's dashboard integration values. Do not invent market labels, product IDs, tokens, or provider configuration.
  • Keep general embedding and native commerce registration separate. Do not rename the existing presenter3d.commerce namespace.
  • Use public methods and returned data, not runtime-internal adapters, cache helpers, scraped viewer DOM, or manually reconstructed requests.
  • Native checkout is independent of 3D readiness. Sharing, scene captures, and PDF generation have stricter readiness requirements.
  • Server review and order state are authoritative. Never derive payable totals from formatted page text or override availability locally.
  • Do not turn project presentation settings into access controls, or treat a shared configuration hash as a buyer/login credential.

WooCommerce and Shopify integration documentation: coming soon.