logo

API

1. Overview

The Orbital Vision 3D Configurator is a self-contained application embedded inside an <iframe>. The parent application can communicate with it to:

  • Load or switch between products or ranges.
  • Select product options (e.g., fabrics, legs, sizes).
  • View or hide product dimensions.
  • Enter AR or VR modes (where supported).
  • Listen for updates (e.g., current price, SKU, etc.).

Communication is done via the standard Window.postMessage API.


2. iFrame URL Requirements

When embedding the configurator, you must include:

  1. Your API key, which authenticates the request.

You can create a new API key in your dashboard at app.orbital.vision/api-keys. Please select the Product Configurator Access type from the dropdown list.

  1. Either a product ID or a range ID.

You can find your desired product ID or range ID by going to app.orbital.vision/products. On the lefthand side of the table there is an ID field for both ranges and products.

This requirement gives two possible URL structures:

  1. Product-Based
  2. Range-Based
  3. Authorised Domain

The [API_KEY] ensures that only authorised integrations can load the 3D environment. Without it, the server hosting the configurator will reject the request.

Required <iframe> Attributes

To enable a full range of features—particularly WebXR (AR/VR) support—the following allow attributes are required:

<iframe 
  src="https://configurator.orbital.vision/[API_KEY]/[PRODUCT_OR_RANGE_PATH]"
  allow="
    accelerometer;
    autoplay;
    clipboard-write;
    encrypted-media;
    gyroscope;
    picture-in-picture;
    xr-spatial-tracking;
    fullscreen"
  style="width: 100%; height: 100%;"
></iframe>
  • accelerometer: Allows the 3D configurator to access device orientation data, used for augmented reality interactions on mobile.
  • autoplay: Permits automatic playback of any embedded 3D or media assets without explicit user interaction.
  • clipboard-write: Allows copying data to the user's clipboard. This may be used for future feature support (for example, quickly sharing config data).
  • encrypted-media: Enables playback of protected or DRM-encrypted content if the configurator requires secure media streams.
  • gyroscope: Grants access to motion sensors needed for advanced AR or VR features on mobile devices.
  • picture-in-picture: Allows the configurator to enter picture-in-picture mode in future updates.
  • xr-spatial-tracking: Essential for AR or VR sessions under WebXR, enabling correct spatial tracking of the virtual object in a real-world environment.
  • fullscreen: Permits the configurator to be viewed in full screen, improving the immersive 3D or AR/VR experience.

3. Initial Handshake: Messages from the Configurator

When the configurator finishes loading, it will send messages to the parent window detailing its initial state. These messages will then resend any time the data in the previous messages becomes stale.

ALL_PRODUCTS

Description:

A complete list of products the configurator can display at load time, this will have a single product only if you did not use the range path.

Example:

{
  "type": "ALL_PRODUCTS",
  "payload": [
    {
      "dimensionX": 250,
      "dimensionY": 100,
      "dimensionZ": 100,
      "id": 1,
      "manufacturerId": 10,
      "name": "Sofa A",
      "price": 120000,
      "pricing": {
        "Leather": {
          "stne": 2000,
          "tan": 2500,
          "black": 2500,
          // etc...
        },
        "Legs": {
          "oak": 2000,
          "metal": 2500,
          // etc...
        },
        "Products": {
          "Example Sofa": 10000,
          "Example Large Sofa": 15000,
          // etc...
        }
      },
      "productId": 66,
      "retailerAccess": true,
      "retailerId": 9,
      "sku": "example-sofa"
    },
    {
      "dimensionX": 200,
      "dimensionY": 80,
      "dimensionZ": 80,
      "id": 2,
      "manufacturerId": 11,
      "name": "Sofa B",
      "price": 150000,
      "pricing": {
        "Leather": {
          "red": 3000,
          "brown": 1000,
          // etc...
        },
        "Legs": {
          "oak": 1000,
          "metal": 2000,
          // etc...
        },
        "Products": {
          "Example Sofa B": 7000,
          "Example Large Sofa B": 9000,
          // etc...
        }
      },
      "productId": 67,
      "retailerAccess": true,
      "retailerId": 9,
      "sku": "example-sofa-b"
    }
  ]
}
  • price and pricing are always in the lowest form of that currency, for example in pence (e.g., 150000 = £1,500.00).

RANGE

Description:

Information about the range currently in use (if you're loading a range-based URL).

Example:

{
  "type": "RANGE",
  "payload": {
    "id": 99,
    "name": "Paris Collection",
    "sku": "paris-collection"
  }
}
  • id matches the [RANGE_ID] passed in the URL.

CURRENT_PRODUCT_ID

Description:

Tells you which specific product is currently loaded by the configurator.

Example:

{
  "type": "CURRENT_PRODUCT_ID",
  "payload": 1
}
  • payload is a simple number in this case (the product ID).

CONFIGURATOR_STATE

Description:

The complete structure of the configurator, detailing available options, groups, selections, and the user's current selections.

Quite often the group name will be 'Default Group' or 'Default' if there is only one group, so when designing your UI we recommend hiding groups and just showing the selections directly nested within their option when there exists only a single group.

Example:

{
  "type": "CONFIGURATOR_STATE",
  "payload": {
    "retailerId": 9,
    "screenshotThumbnail": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/screenshot/productId/100_uuid.webp",
    "configuratorSettings": {
      "availableProductFilters": {
        "Legs": ["Wood Type", "Leg Size"],
        "Fabric": ["Colour", "Type", "Pattern"],
        "Trims": [],
      },
      "defaultSelections": [
        {
          "optionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
          "groupId": "6ac505da-3270-4b42-98b3-9b54bcae4d36",
          "selectionId": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4"
        },
        {
          "optionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
          "groupId": "2d6eec44-daa1-407d-bb31-ee06a06660ed",
          "selectionId": "84401d85-25eb-4eef-9e7e-7c91679d4cf1"
        }
      ],
      "visibleSelections": [
        {
          "optionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
          "groupId": "6ac505da-3270-4b42-98b3-9b54bcae4d36",
          "selectionId": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4"
        },
        // etc...
      ]
    },
    "options": [
      {
        "id": "4fe89411-d6b9-4a67-bbcc-905eab673770",
        "name": "Legs",
        "isRequired": true,
        "uuid": "4fe89411-d6b9-4a67-bbcc-905eab673770",
        "groups": [
          {
            "configuratorOptionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
            "id": "6ac505da-3270-4b42-98b3-9b54bcae4d36",
            "name": "default",
            "selections": [
              {
                "blurHash": "LkEoj[ofR*WBM{WBt7ayS2ayof",
                "configuratorGroupId": "6ac505da-3270-4b42-98b3-9b54bcae4d36",
                "configuratorOptionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
                "id": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4",
                "filter": {
                  "Wood Type": ["Oak"],
                  "Leg Size": ["Small"]
                },
                "miniThumbnails": {
                  "large": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/250_uuid.webp",
                  "medium": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/150_uuid.webp",
                  "small": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/50_uuid.webp"
                },
                "name": "Oak",
                "sku": "LEGS_OAK",
                "thumbnail": "https://cdn.example.com/images/oak-legs.jpg",
                "uuid": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4"
              },
              {
                "blurHash": "LLHV}tay~qa#?wM{RjozofM{WBRj",
                "configuratorGroupId": "6ac505da-3270-4b42-98b3-9b54bcae4d36",
                "configuratorOptionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
                "id": "84401d85-25eb-4eef-9e7e-7c91679d4cf1",
                "filter": {
                  "Wood Type": ["Metal"],
                  "Leg Size": ["Medium"]
                },
                "miniThumbnails": {
                    "large": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/250_uuid.webp",
                    "medium": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/150_uuid.webp",
                    "small": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/50_uuid.webp"
                },
                "name": "Metal",
                "sku": "LEGS_METAL",
                "thumbnail": "https://cdn.example.com/images/metal-legs.jpg",
                "uuid": "84401d85-25eb-4eef-9e7e-7c91679d4cf1"
              }
            ]
          }
        ]
      },
      {
        "id": "172b84ab-2834-41ec-88b4-0ae3ef2776c4",
        "name": "Fabric",
        "isRequired": true,
        "uuid": "172b84ab-2834-41ec-88b4-0ae3ef2776c4",
        "groups": [
          {
            "configuratorOptionId": "172b84ab-2834-41ec-88b4-0ae3ef2776c4",
            "id": "2d6eec44-daa1-407d-bb31-ee06a06660ed",
            "name": "Velvet",
            "selections": [
              {
                "blurHash": "LkEoj[ofR*WBM{WBt7ayS2ayof",
                "configuratorGroupId": "2d6eec44-daa1-407d-bb31-ee06a06660ed",
                "configuratorOptionId": "172b84ab-2834-41ec-88b4-0ae3ef2776c4",
                "id": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4",
                "filter": {
                  "Fabric Type": ["Velvet"],
                },
                "miniThumbnails": {
                    "small": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/50_uuid.webp",
                    "medium": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/150_uuid.webp",
                    "large": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/250_uuid.webp"
                },
                "name": "Blue Fabric",
                "sku": "FAB_BLUE",
                "thumbnail": "https://cdn.example.com/images/fabric-blue.jpg",
                "uuid": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4"
              },
              {
                "blurHash": "LQI6j@ayxuxu%MD*i^RkxuRkRjWB",
                "configuratorGroupId": "2d6eec44-daa1-407d-bb31-ee06a06660ed",
                "configuratorOptionId": "172b84ab-2834-41ec-88b4-0ae3ef2776c4",
                "id": "84401d85-25eb-4eef-9e7e-7c91679d4cf1",
                "filter": {
                  "Fabric Type": ["Plush"],
                },
                "miniThumbnails": {
                    "small": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/50_uuid.webp",
                    "medium": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/150_uuid.webp",
                    "large": "https://bucket-name.s3.region.amazonaws.com/configurator/thumbnails/miniThumbs/productId/250_uuid.webp"
                },
                "name": "Red Fabric",
                "sku": "FAB_RED",
                "thumbnail": "https://cdn.example.com/images/fabric-red.jpg",
                "uuid": "84401d85-25eb-4eef-9e7e-7c91679d4cf1"
              }
            ]
          }
        ]
      }
    ],
    "selectedSelections": [
      {
        "optionId": "4fe89411-d6b9-4a67-bbcc-905eab673770",
        "groupId": "6ac505da-3270-4b42-98b3-9b54bcae4d36",
        "selectionId": "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4"
      },
      {
        "optionId": "172b84ab-2834-41ec-88b4-0ae3ef2776c4",
        "groupId": "2d6eec44-daa1-407d-bb31-ee06a06660ed",
        "selectionId": "84401d85-25eb-4eef-9e7e-7c91679d4cf1"
      }
    ]
  }
}
  • Each selection comes with a thumbnail, and mini thumbnails. The mini thumnails are sized 50px, 150px and 250px (small, medium large)
  • They are all webp which as you can see from https://caniuse.com/webp is supported in nearly all browsers. In 2025 it is completely safe to use and we recommend that you use these performant mini thumbnails. If you do not want to use them, there is also the original thumbnail jpg for you to use with an image service/cdn.
  • Each option can contain one or more groups. Each group can contain one or more selections.
  • selectedSelections tells you exactly which selectionId the user has chosen for each option/group.

CURRENT_PRICE

Description:

The calculated price based on the user's current selections.

Example:

{
  "type": "CURRENT_PRICE",
  "payload": {
    "formattedPrice": "£1,036.00",
    "totalPrice": 103600,
    "subtotal": 129500,
    "formattedSubtotal": "£1,295.00",
    "discount": {
      "amount": 25900,
      "formattedAmount": "£259.00",
      "percentage": 20
    },
    "priceBreakdown": [
      {
        "category": "Ranges",
        "formattedPrice": "£0.00",
        "name": "Paris",
        "price": 0,
        "sku": "paris-range"
      },
      {
        "category": "Products",
        "formattedPrice": "£1,200.00",
        "name": "Example Sofa",
        "price": 120000,
        "sku": "example-sofa"
      },
      {
        "category": "Leather",
        "formattedPrice": "£300.00",
        "name": "Red",
        "price": 30000,
        "sku": "LEATHER_RED"
      },
    ]
  }
}
  • formattedPrice is a user-facing string (localized).
  • totalPrice is an integer in the lowest form of that currency (103600 = £1,036.00).
  • subtotal is the price before any discounts are applied (129500 = £1,295.00).
  • formattedSubtotal is the formatted string representation of the subtotal.
  • discount contains the discount information with amount (in lowest currency form), formattedAmount (localized string), and percentage (discount percentage).
  • priceBreakdown is a breakdown of the total price, including the price of the range, product, and any selections such as fabric or legs. This will sum to subtotal.

CURRENT_SKU

Description:

The SKU representing the current product configuration. Helpful for backend or e-commerce tracking.

Example:

{
  "type": "CURRENT_SKU",
  "payload": {
    "skuString": "WINDRUSH/GRAND/LEGS_OAK/FAB_RED",
    "skuMap": {
	    "Range": "WINDRUSH",
	    "Product": "GRAND",
      "Legs": "LEGS_OAK",
      "Fabric": "FAB_RED"
    }
  }
}
  • skuString is a combination of selected options/selections.
  • skuMap shows how each option name maps to the underlying SKU.

Description:

A direct link to an AR experience, used when the current device is not appropriate.

Example:

{
  "type": "AR_PREVIEW_LINK",
  "payload": "https://configurator.orbital.vision/ar-preview/{key}"
}
  • It is recommended that you display this link with a QR code to scan from a mobile device.
  • On iOS devices, this will open AR Quick Look. Unfortunately apple refuses to support other methods such as webXR which are otherwise universally supported. Additionally, USDZ does not have full support for lighting/custom shaders and there is also a hard limit on textures that we can use in the material. This leads to a huge loss of quality when using AR on any IOS device, such as an iPhone. We are actively monitoring changes from apple and if it becomes possible in the future, we will address this.

How to Use These Messages

  • All messages are posted to the parent window. You can intercept them with:

    window.addEventListener("message", (event) => {
      const { type, payload } = event.data;
      // Parse and handle each type accordingly
    });
  • Parse payload with JSON.parse(...) if the configurator sends it as a string.

  • Update your UI or application state with product data, pricing, SKU, or AR links as needed.


4. Listening for Messages (Parent Application)

In your parent application, add an event listener for message:

 
window.addEventListener("message", (event) => {
  const { type, payload } = event.data;
  if (!type) return;
 
  // Safely parse payload
  let data;
  try {
    data = JSON.parse(payload);
  } catch (error) {
    console.error("Failed to parse configurator message payload:", payload);
    return;
  }
 
  switch (type) {
    case "ALL_PRODUCTS":
      // e.g. store in your state
      console.log("All products:", data);
      break;
    case "CURRENT_PRICE":
      console.log("Updated price:", data);
      break;
    case "CURRENT_PRODUCT_ID":
      console.log("Active product ID:", data);
      break;
    // ... handle other message types
  }
});

5. Sending Messages to the Configurator

You can control the configurator by sending messages from the parent app to the iframe's contentWindow. For example:

const iframeEl = document.getElementById("configurator-iframe");
 
function sendToConfigurator(type, payload) {
  iframeEl.contentWindow.postMessage(
    { type, payload: JSON.stringify(payload) },
    "*"
  );
}

Below are the most common commands:

  1. SELECT_PRODUCT

    Instructs the configurator to switch to another product:

    sendToConfigurator("SELECT_PRODUCT", 2);
  2. SELECT_SELECTION

    Applies a user's choice (e.g., new fabric or color) for a specific option:

    sendToConfigurator("SELECT_SELECTION", {
      optionId: "4fe89411-d6b9-4a67-bbcc-905eab673770",
      groupId: "6ac505da-3270-4b42-98b3-9b54bcae4d36",
      selectionId: "0fa4b7e2-caf5-4d6b-9400-3c612427eaf4"
    });
  3. VIEW_DIMENSIONS

    Shows or hides dimension lines on the 3D model:

    sendToConfigurator("VIEW_DIMENSIONS", { dimensions: true });
    // or
    sendToConfigurator("VIEW_DIMENSIONS", { dimensions: false });
  4. ENTER_AR

    Attempts to start an augmented reality session or, if necessary, returns an AR_PREVIEW_LINK that you can display:

    sendToConfigurator("ENTER_AR", {});
  5. ENTER_VR

    (If supported) instructs the configurator to begin a VR session:

    sendToConfigurator("ENTER_VR", {});

6. Configurator → Parent Message Types (Detailed)

Below is a reference table for all inbound messages the configurator sends and their payload content:

TypeMeaningPayload (JSON)
ALL_PRODUCTSFull list of available products.Array of product objects, each with dimensionX, dimensionY, dimensionZ, id, manufacturerId, name, price, pricing, productId, retailerAccess, retailerId, sku and other data.
RANGEData about the current range or collection.An object with id, name, skuand possibly other range-specific fields.
CURRENT_PRODUCT_IDIndicates which product is actively displayed.A single number (e.g., 45).
CONFIGURATOR_STATEThe complete nested structure of options, groups, selections, etc.An object that includes retailerId, screenshotThumbnail, configuratorSettings (object), options (array) and selectedSelections (array).
CURRENT_PRICEThe item's total price based on user selections.An object with formattedPrice (e.g., "£1,200") and totalPrice (the numeric value in the lowest form of that currency IE pence), and priceBreakdown (array of objects with category, formattedPrice, name, price, sku).
CURRENT_SKUThe SKU representing the user's current configuration.An object with skuString (e.g., "SOFA/GREY/OAK") and skuMap (an object of option name → sku).
AR_PREVIEW_LINKA direct link to an AR preview for compatible devices.A string containing the URL.
SELECT_PRODUCT_RECEIVEDConfirms the configurator has received and processed a SELECT_PRODUCT command.Typically the same ID you sent (useful for showing loading states).
ANIMATION_STATENotifies the parent of the current animation state.A string representing the current animation state (e.g., "unavailable", "stop", "loop", "open", "close").

7. Parent → Configurator Message Types (Detailed)

Below is a reference table for outbound messages you can send to the configurator:

TypeDescriptionExpected Payload
SELECT_PRODUCTLoads a specific product by its ID.A single integer representing the product ID.
SELECT_SELECTIONUpdates the user's choice for a specific option.An object with { optionId: string, groupId: string, selectionId: string }.
VIEW_DIMENSIONSShows or hides product dimension lines.An object with { dimensions: boolean }.
ENTER_ARInitiates augmented reality mode, if supported.An empty object {} or custom data if needed.
ENTER_VRTries to start a virtual reality session, if supported.An empty object {}.
TOGGLE_ANIMATIONCycles through animation states (stop→loop, loop→stop, open→close, close→open).An empty object {}. Note: Affects all animation mixers in the scene.

8. Typical Integration Flow

  1. Embed the iFrame
    • Provide the correct URL (with your API key + product or range).
    • Include the required allow attributes for AR/VR, fullscreen, etc.
  2. Listen for the Handshake
    • As soon as the iframe loads, it sends ALL_PRODUCTS, CURRENT_PRODUCT_ID, CONFIGURATOR_STATE, etc.
    • Parse and store these details (e.g., in your parent app's state).
  3. User Interaction in the Parent
    • When a user selects a different product from your UI:

      sendToConfigurator("SELECT_PRODUCT", newProductId);
    • The configurator reloads to that product and returns updated messages (price, SKU, etc.).

  4. Configurator Updates
    • On any selection change, the configurator posts back CURRENT_PRICE, CURRENT_SKU, etc.
    • Update your parent UI or e-commerce logic accordingly.
  5. Special Actions
    • AR: ENTER_AR leads to a WebXR session on Android or an AR_PREVIEW_LINK for iOS.
    • Dimensions: Toggle rendering of dimension helpers on or off via VIEW_DIMENSIONS.

9. Example Minimal Code

Below is an extremely simplified example (plain HTML + JS) showing a typical integration:

<!DOCTYPE html>
<html>
  <head>
    <title>3D Configurator Integration</title>
    <style>
      #configurator-frame {
        width: 800px;
        height: 600px;
        border: 1px solid #ccc;
      }
    </style>
  </head>
  <body>
    <h1>Configurator Integration Demo</h1>
 
    <!-- Replace 'abc123' and '45' with your API key and product/range details -->
    <iframeid="configurator-frame"
      src="https://configurator.orbital.vision/abc123/45"
      allow="
        accelerometer;
        autoplay;
        clipboard-write;
        encrypted-media;
        gyroscope;
        picture-in-picture;
        xr-spatial-tracking;
        fullscreen
      "
    ></iframe>
 
    <div style="margin-top: 20px;">
      <button id="selectProductBtn">Select Product #2</button>
      <button id="toggleDimensionsBtn">Toggle Dimensions</button>
      <button id="enterARBtn">Enter AR</button>
    </div>
 
    <script>
      const iframeEl = document.getElementById("configurator-frame");
      let showDimensions = false;
 
      function sendToConfigurator(type, payload) {
        iframeEl.contentWindow.postMessage(
          { type, payload: JSON.stringify(payload) },
          "*"
        );
      }
 
      window.addEventListener("message", (event) => {
        const { type, payload } = event.data;
        if (!type) return;
 
        let data;
        try {
          data = JSON.parse(payload);
        } catch (e) {
          console.error("Error parsing payload:", payload);
          return;
        }
 
        switch (type) {
          case "ALL_PRODUCTS":
            console.log("All products:", data);
            break;
          case "CURRENT_PRICE":
            console.log("Current price:", data);
            break;
          case "CURRENT_PRODUCT_ID":
            console.log("Active product ID:", data);
            break;
          case "AR_PREVIEW_LINK":
            console.log("AR link:", data);
            // Possibly show a QR code for desktop users
            break;
          // ... other message handlers ...
        }
      });
 
      document.getElementById("selectProductBtn").addEventListener("click", () => {
        sendToConfigurator("SELECT_PRODUCT", 2);
      });
 
      document.getElementById("toggleDimensionsBtn").addEventListener("click", () => {
        showDimensions = !showDimensions;
        sendToConfigurator("VIEW_DIMENSIONS", { dimensions: showDimensions });
      });
 
      document.getElementById("enterARBtn").addEventListener("click", () => {
        sendToConfigurator("ENTER_AR", {});
      });
    </script>
  </body>
</html>

10. Tips & Troubleshooting

  • Cross-Origin: Ensure you have the correct CORS or target origins set.
  • Performance: Since real-time 3D rendering can be resource-intensive, consider deferring the iframe load until it's needed, or show a placeholder until loading completes. A good idea can be showing an image of the product, then when the user selects a configuration item switching to the iframe. This way the iframe will have already loaded in the second or two it takes them to make a selection, hiding the loading state.
  • AR/VR Compatibility: The ENTER_AR and ENTER_VR features rely on the browser's WebXR implementation. Unsupported devices may fall back to returning an AR_PREVIEW_LINK.
  • Event Parsing: Always wrap JSON.parse(payload) in a try/catch block to handle any unexpected responses gracefully.
  • Product Switches: After sending SELECT_PRODUCT, wait until you receive an update before updating the UI, as our system debounces updates.