For the people wiring it up.

LensAdvisor slots into existing Shopify operations rather than replace them. This guide covers install paths, event hooks, the API, and the metafield keys that drive lens catalogue logic.

API version2025-04
AuthApp Proxy
Rate limit40 / s
01 · Install paths

Two install paths. One flow.

OS 2.0 themes install via app block. OS 1.0 themes use a loader snippet. Both paths end up running the same prescription flow.

Online Store 2.0 (recommended)

Add the LensAdvisor Select Lenses app block to your product template via the Shopify theme editor. Save. That is the whole install. Repeat on the cart template so bundled line items stay in sync.

For a Quick Buy button anywhere else (home, collections, custom sections), turn on the LensAdvisor Quick Buy app embed and drop the web component into your theme code.

liquid
<lensadvizor-quick-buy
    class="la-select-lenses-btn"
    data-la-quick-buy
    data-la-product-id="{{ product.id }}"
    data-la-variant-id="{{ product.selected_or_first_available_variant.id }}"
    data-la-product-json="{{ product | json | escape }}"
    data-la-flow-id=""
>
</lensadvizor-quick-buy>

Online Store 1.0 (manual install)

Download lensadvizor.liquid, drop it into snippets/, then render it from theme.liquid on the product and cart page types.

liquid
<!-- LensAdvizor snippet starts -->
{% if request.page_type == 'product' or request.page_type == 'cart' %}
  {% render 'lensadvizor' %}
{% endif %}
<!-- LensAdvizor snippet ends -->

Excluding products

Add the tag remove-rx to any frame product to suppress the Select Lenses button on that product, even if it matches a Lens Flow by collection.

Tip

When the Quick Buy component is rendered dynamically (section rendering, AJAX-loaded product cards), add data-la-quick-view="1" or call LensAdvizor.init() after insert.

02 · Event system

Hook into the flow.

Every meaningful step in the customer flow emits a CustomEvent. Listen for analytics, CRM sync, or UX tweaks.

LensAdvisor emits CustomEvents on document throughout the flow. Listen for analytics, CRM sync, or UX tweaks.

Lifecycle

  • LensAdvizor:init:start initialisation begins
  • LensAdvizor:init:complete fully loaded, flow is interactive

Customer flow

  • LensAdvizor:prescriptionTypes:rendered prescription type selector visible
  • LensAdvizor:lens:rendered lens options visible
  • LensAdvizor:review:rendered review screen visible
  • LensAdvizor:addToCart:success prescription added to cart
  • LensAdvizor:postAddToCart fires after cart update for drawer integrations
js
document.addEventListener('LensAdvizor:addToCart:success', (e) => {
  const { product, prescription } = e.detail;
  gtag('event', 'rx_added_to_cart', {
    product_id: product.id,
    rx_type: prescription.prescriptionType,
    value: product.price / 100,
    currency: Shopify.currency.active,
  });
});

Cart drawer integration

LensAdvisor ships integrations for Dawn, Vision, Focal, Impulse, Flow, Impact, Release, Stiletto, UpCart, Rebuy, and iCart. For custom themes, enable the postAddToCart event and listen for it to re-render your cart drawer.

js
document.addEventListener('LensAdvizor:postAddToCart', async () => {
  const cart = await fetch('/cart.js').then((r) => r.json());
  window.myTheme.cartDrawer.render(cart);
  window.myTheme.cartDrawer.open();
});
Note

Event names use the legacy spelling LensAdvizor (with a 'z'). This is intentional. Renaming would break every merchant customisation in production.

03 · API

REST over app proxy.

Read prescription orders, inspect the lens catalogue, push fulfilment status. Authenticated via Shopify App Proxy with no access token required.

The API is available on Pro and above. Requests are authenticated via Shopify App Proxy signature so you can call it directly from your storefront without exposing an access token.

Endpoints

  • GET /apps/lensadvisor/orders list prescription orders for this shop
  • GET /apps/lensadvisor/orders/:id a single order with full prescription payload
  • GET /apps/lensadvisor/lens-catalog configured lens catalogue
  • POST /apps/lensadvisor/orders/:id/status update fulfilment status from your ops system
bash
curl 'https://{shop}.myshopify.com/apps/lensadvisor/orders?limit=50' \
  -H 'Accept: application/json'

Sample response

json
{
  "orders": [
    {
      "id": "ord_01J9X7",
      "shopify_order_id": 5412345678901,
      "prescription_type": "progressive",
      "prescription": {
        "od": { "sphere": -2.25, "cylinder": -0.50, "axis": 180 },
        "os": { "sphere": -2.00, "cylinder": -0.75, "axis": 175 },
        "add": 1.75,
        "pd": 62
      },
      "lens": { "handle": "blue-light-plus", "variant_id": 49128374 },
      "matrix_image_url": "https://cdn.lensadvisor.com/matrix/...",
      "status": "awaiting_lab"
    }
  ],
  "pagination": { "next_cursor": "..." }
}
Rate limit

40 requests per second per shop. Exceed and the response returns 429 with Retry-After. Enterprise plans can raise the limit on request.

04 · Metafields

The keys that drive the flow.

Collection, product, and shop metafields in the lensadvisor namespace. Plus the handful of LensAdvisor-native keys you set on lenses, groups, and prescription types via the admin UI.

LensAdvisor reads a small set of Shopify metafields to configure per-collection, per-product, and per-variant behaviour. Write them via Shopify admin (Custom data), the Bulk Editor, or the Admin API.

Collection-scoped

  • lensadvisor.collection_id (single_line_text) — the id of the LensAdvisor Lens Flow to run for every product in this collection. Set this once on a collection and every product in it picks up the flow automatically.

Product-scoped

  • lensadvisor.options (json) — per-product overrides for the manual-entry form: PD range, dual PD range, sphere range, segment height defaults, plus a switch to hide segment-height fields. See the example below.
  • lensadvisor._assigned_variants (json) — managed automatically by the LensAdvisor admin UI when you assign lens flows per-variant. Not intended for manual authoring.

Shop-scoped (app-managed)

  • lensadvisor.BASE_URL (single_line_text) — storefront bootstrap URL, written by the app on install.
  • lensadvisor._global_settings (json) — serialised storefront config, managed through the LensAdvisor admin.
json
{
  "prescriptionConfig": {
    "fieldOptions": {
      "pd":            { "min": "50", "max": "72", "steps": "0.5", "defaultValue": "64" },
      "dualPd":        { "min": "25", "max": "36", "defaultValue": "32" },
      "sph":           { "min": "-4.00", "max": "2.00" },
      "segmentHeight": { "defaultValue": "16" }
    },
    "segmentHeight":         "hidden",
    "segmentHeightOnUpload": "hidden"
  }
}

Lens-level metafields

Lenses, lens groups, prescription types, and add-ons are LensAdvisor-native objects, not Shopify products. Their metafields are written through the ⠇ (kebab) → Edit metafields menu inside the LensAdvisor admin, not through Shopify admin.

  • _showRecommendationBadge (boolean) — when true, surfaces a "recommended" badge on a lens row. Keys prefixed with _ are hidden from Orders.
  • toolTip (string) — hover help text shown on a lens or lens group.
  • picture_matrix (json) — maps frame product ids to composited matrix preview images for a lens variant.
  • addon_flow (string, prescription-type metafield) — id of a secondary Lens Flow that runs as add-ons after the main selection.
Note

Metadata keys that start with an underscore (_hideIfCountryIs, _showRecommendationBadge, etc.) are hidden from the Orders view in the LensAdvisor admin and from Shopify order properties. Use this convention for anything that's purely runtime configuration the buyer doesn't need to see.

05 · Developer recipes

Working examples.

Copy-paste snippets covering events, analytics, metadata, custom properties, UI tweaks, and API access. Pulled from the legacy developer FAQ, updated for the current storefront.

05 Developer recipes
01 LensAdvisor V2 Events

Our goal is for every customer to be totally satisfied with their purchase. If this isn't the case, let us know and we'll do our best to work with you to make it right.

document.addEventListener('LensAdvizor:init:start', function() {});
document.addEventListener('LensAdvizor:init:complete', function() {});
document.addEventListener('LensAdvizor:init:error', function() {});

document.addEventListener('LensAdvizor:variant:change', function() {});
document.addEventListener('LensAdvizor:lensVariant:change', function() {});

document.addEventListener('LensAdvizor:selectLensModal:open', function() {});
document.addEventListener('LensAdvizor:selectLensModal:close', function() {});

document.addEventListener('LensAdvizor:prescriptionTypes:rendered', function() {});

document.addEventListener('LensAdvizor:render:contactLens', function() {});

document.addEventListener('LensAdvizor:render:submissionMethods', function() {});

document.addEventListener('LensAdvizor:render:uploadForm', function() {});
document.addEventListener('LensAdvizor:render:readingForm', function() {});
document.addEventListener('LensAdvizor:render:manualEntryForm', function() {});

document.addEventListener('LensAdvizor:lens:rendered', function() {});
document.addEventListener('LensAdvizor:lens:rendered', function(e) {

// check e.detail.method for step
});

document.addEventListener('LensAdvizor:lens:rendered', function(e) {
    if (e.detail.method == 'renderLensGroup') {
        // handle renderLensGroup
    }
    if (e.detail.method == 'renderLenses') {
        // handle renderLenses
    }
});

document.addEventListener('LensAdvizor:lensOptions:rendered', function() {});
document.addEventListener('LensAdvizor:lensOption1:rendered', function() {});
document.addEventListener('LensAdvizor:lensOption2:rendered', function() {});
document.addEventListener('LensAdvizor:lensOption3:rendered', function() {});

document.addEventListener('LensAdvizor:addOns:rendered', function() {});

document.addEventListener('LensAdvizor:review:rendered', function() {});

document.addEventListener('LensAdvizor:action:exception', function() {});

document.addEventListener('LensAdvizor:addToCart:success', function() {});
document.addEventListener('LensAdvizor:addToCart:error', function() {});

document.addEventListener('LensAdvizor:prescription:updated', function() {});
document.addEventListener('LensAdvizor:toolTip:open', function() {});
document.addEventListener('LensAdvizor:Cart:Stable', function() {});
document.addEventListener('LensAdvizor:Cart:Updated', function() {});
02 Integrating Analytics

The following is an example of integrating Google Analytics 4 (GA4), which can be adapted for any analytics software.

Copy and paste this code into LensAdvisor App > Settings > Advanced: Custom JS


// ---Setup tracking of Virtual Pageviews for Google Analytics 4--- 
document.addEventListener('LensAdvizor:prescriptionTypes:rendered', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/select_prescription_type' });
});
document.addEventListener('LensAdvizor:render:submissionMethods', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/select_prescription_method' });
});
document.addEventListener('LensAdvizor:render:uploadForm', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/enter_prescription_uploaded' });
});
document.addEventListener('LensAdvizor:render:manualEntryForm', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/enter_prescription_manually' });
});
document.addEventListener('LensAdvizor:lens:rendered', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/choose_lens' });
});
document.addEventListener('LensAdvizor:lensOptions:rendered', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/choose_options' });
});
document.addEventListener('LensAdvizor:addOns:rendered', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/choose_addons' });
});
document.addEventListener('LensAdvizor:review:rendered', function(){
    gtag('event', 'page_view', { 'page_path': '/lensadvisor/review_selection' });
});
03 The LensAdvizor.response object

As the customer goes through the LensAdvisor flow, we add information to the LensAdvizor.response object. You can use this object to view what the customer has already chosen in order to run custom code.

04 Adding Metadata

Metadata can be added to any object in LensAdvisor as a key:value pair. Metadata entry can
be found in the ( ⠇) sub menu to the right of each object in the flow.

This can be useful to pass data to the order for identification during fulfilment, or for custom code during the LensAdvisor flow.

For instance, one store uses a key of _hideIfCountryIs (with the value being the country code) in order to hide certain elements in the flow when the country matches.

05 Storefront Metadata Objects

You can find metadata that has been set in the flows editor in the following storefront javascript objects.

Once the modal has loaded
Flow: LensAdvizor.options.flowMetafields
Prescription Type: LensAdvizor.prescriptionTypes[n].metafields
Example:
to get the current prescription type metadata >
LensAdvizor.prescriptionTypes.find(type => type.name === LensAdvizor.response.prescriptionType).metafields

Once lenses have loaded
Lens Group: LensAdvizor.lensGroups[n].metafields
Lens: LensAdvizor.lenses[n].metafields
(or through the LensAdvizor.lensesByGroups object)
Lens Option: LensAdvizor.lenses[n].raw_options[n].metafields
Add Ons: LensAdvizor.lenses[n].add_ons_products[n].metafields

---

Note: Metadata keys that start with an underscore will not be shown in LensAdvisor > Orders.

06 Adding Lab Metadata

Metadata can be added to any object in LensAdvisor as a key:value pair. Metadata entry can
be found in the ( ⠇) sub menu to the right of each object in the flow.

This can be useful to pass data to the order for identification during fulfilment, or for custom code during the LensAdvisor flow.

For instance, one store uses a key of _hideIfCountryIs (with the value being the country code) in order to hide certain elements in the flow when the country matches.

Lab Metadata

Lab Metadata is a series of key:value pairs.

This data will be used to modify the request to the selected lab's API.

Example:

Key: RELensMaterial
Value: PO-58-NONE-NONE-00

Calculating and Cascading Metadata

By default, if the same key exists at a lower level in the metadata chain, the value of the lower level will replace the value of the higher level for the same key.

Example:

Lens Level:
Key: RELensMaterial
Value: PO-58-NONE-NONE-00

Lens Option Level:
Key: RELensMaterial
Value: PO-67-POLR-BRWN-00

Final Result:
Key: RELensMaterial
Value: PO-67-POLR-BRWN-00

Using Variables in Metadata

Metadata can have variables where the lower level of metadata will instead replace parts of the higher level of metadata. This is accomplished by 'creating a variable' and 'setting a variable'.

Creating a Variable in a String

This is done by using the format of [variable=value].

Setting a Variable in a String

This is done by using the format of {{variable=value}}.

Example:

Lens Level:
Key: RELensMaterial
Value: PO-58-[lens_type=NONE]-[lens_color=NONE]-00

Lens Option Level:
Key: RELensMaterial
Value: {{lens_type=POLR}}{{lens_color=GREY}}

Final Result:
Key: RELensMaterial
Value: PO-67-POLR-GREY-00

07 Adding text next to PD dropdowns
// ADD PD TEXT
// Upload Form
document.addEventListener('LensAdvizor:render:uploadForm', function(){
	document.querySelector("#la-pd-fields-container").insertAdjacentHTML("afterend", "<br><p>Inserted Custom HTML</p>")
})
// Reading Form
document.addEventListener('LensAdvizor:render:readingForm', function(){
	document.querySelector("#la-pd-fields-container").insertAdjacentHTML("afterend", "<br><p>Inserted Custom HTML</p>")
})
// Manual Entry Form
document.addEventListener('LensAdvizor:render:manualEntryForm', function(){
	document.querySelector("#la-pd-fields-container").insertAdjacentHTML("afterend", "<br><p>Inserted Custom HTML</p>")
})
08 Adding a required checkbox to forms
document.addEventListener('LensAdvizor:render:manualEntryForm', function(){
  document.querySelector('[data-form=manually] .la-upload-wrapper').insertAdjacentHTML("afterend",`
    <div class='la-rx-verify'> 
      <input type='checkbox' id='la-rx-verify-checkbox' required> 
      <label for='la-rx-verify-checkbox' class='la-rx-verify-label'>
        I confirm I've entered my prescription correctly.
      </label>
    </div>
  `)
})
09 Creating custom tooltips
// First get the layout
let tooltipData = {
    "title":"Tooltip Title",
    "content": "Tooltip Content HTML <h1>awesome</h1>"
}
// imgSrc is optional. If omitted the default image will be used
customToolTipLayout = LensAdvizor.getToolTipLayout(tooltipData, '<imgSrc>')
// insert the tooltip into the HTML
document.querySelector('.some-class').insertAdjacentHTML('afterend', customToolTipLayout)
10 Change the default back and close buttons
// Change the default back and close buttons
document.addEventListener('LensAdvizor:prescriptionTypes:rendered', function(){
	document.querySelector('.la-steeper-back img').src = '<imgURL>'
	document.querySelector('.la-prescription-modal-close img').src = '<imgURL>'
})
11 Change icons
 // Changing icons
document.addEventListener('LensAdvizor:init:complete', function(){
	LensAdvizor.svg.upload = '<svg code>'
})

Icons available to change:

  • LensAdvizor.svg.upload
  • LensAdvizor.svg.edit
  • LensAdvizor.svg.email
  • LensAdvizor.svg.attachment
  • LensAdvizor.svg.check
  • LensAdvizor.svg.tooltip
  • LensAdvizor.svg.preview
  • LensAdvizor.svg.doc
  • LensAdvizor.svg.onfile
  • LensAdvizor.svg.camera

12 Custom Property Inputs

It's possible to add custom properties. This is data that will be added to the Lens product's properties. Any HTML input with 'la-properties[name]', where 'name' is any name that you choose, will be added to the Lens as a custom property.

document.addEventListener('LensAdvizor:prescriptionTypes:rendered', function(){
    // When the Prescription Types are created, add in the hidden input(s)
    hiddenInputText = `
      <input id="hiddenTryOnProperty" type="hidden" name='la-properties[Try On]' value="Yes" >
    `
    wrapper = document.querySelector('.la-prescription-modal-wrapper')
    wrapper.insertAdjacentHTML('beforeend', hiddenInputText)    
})
13 Custom Properties Javascript

You can add custom properties by adding key: value pairs to the following objects in Javascript.

Frame:
LensAdvizor.customBaseProductProperties

Lenses:
LensAdvizor.customLensProperties

Add-Ons:
LensAdvizor.customAddOnsProperties

 // Changing icons
document.addEventListener('LensAdvizor:init:complete', function(){
	LensAdvizor.svg.upload = '<svg code>'
})

Icons available to change:

  • LensAdvizor.svg.upload
  • LensAdvizor.svg.edit
  • LensAdvizor.svg.email
  • LensAdvizor.svg.attachment
  • LensAdvizor.svg.check
  • LensAdvizor.svg.tooltip
  • LensAdvizor.svg.preview
  • LensAdvizor.svg.doc
  • LensAdvizor.svg.onfile
  • LensAdvizor.svg.camera
14 Custom Add On Products

It is possible to add custom Add On Products to the response via custom code

 // ADD A Custom Add On product 
	LensAdvizor.customAddOns.push({
		productId: 12345678901234, // Change to desired Product ID
		variantId: 12345678901234,// Change to desired variant ID of Product
		step: "Example Step",
		productTitle: "Example - $19 Extra",
		price: 19,
		properties: {}
	})

This is a helper function to remove a custom Add On step by step name.

 // Helper fuction to remove step by name
 var removeCustomAddOnStep = function(stepName){
    const indx = LensAdvizor.customAddOns.findIndex(v => v.step === stepName);
    LensAdvizor.customAddOns.splice(indx, indx >= 0 ? 1 : 0);
  }
15 API Access

We expose both a Customer and Orders API.

First generate an Access Token by going to your LensAdvizor App > Settings and scroll down to the bottom to generate the Access Token.

Then, read our API Documentation to use our Customer and Orders API

We expose both a Customer and Orders API.First generate an Access Token by going to your LensAdvizor App > Settings and scroll down to the bottom to generate the Access Token.Then, read our API Documentation to use our Customer and Orders API
Building something bespoke?

Get a dev on the call. First call is free.

Custom lab integrations, deep metafield setups, and migration from another prescription app are all scoped live with our engineering team on Pro and above.

Book a dev call