Loading the module

The CDN URLs, the two build flavours, and the complete Handler API.

New integrations should use Saphere Scan v2. What follows documents the v1 module for products already running it.

The two builds

The same widget is published twice, and which one you load decides how you reach it.

BuildURLHow you get Handler
Classic scripthttps://cdn.saphere.ai/saphere-scan/v1/js/main.min.jswindow.Handler, or the handler-ready event
ES modulehttps://cdn.saphere.ai/saphere-scan/v1/mjs/main.min.jsimport { Handler } from …

Both accept an optional ?customizationId=<uuid> query parameter, which applies a customisation configured in the console before your own options are merged on top. The value must be a UUID: anything else makes the CDN refuse the request, and the script does not load. An identifier that matches no v1 customisation is not an error — the module loads without one.

Two older addresses still serve the same module: /saphere-scan/js/main.min.js and /modules/M1/<version>/js/main.min.js, with their mjs counterparts. They are deprecated; move to the /saphere-scan/v1/ paths above.

The URLs above are Production. Only the host changes between environments; the paths are identical:

EnvironmentCDN base
Productionhttps://cdn.saphere.ai
Testhttps://cdn.test.saphere.ai

As with v2, the CDN you load from is what decides which environment the module works against. Environments covers the rest.

The ES module build

Preferred where you control the page and can use modules.

<script type="module">
    import { Handler } from "https://cdn.saphere.ai/saphere-scan/v1/mjs/main.min.js"

    Handler.load("#container", options)
</script>

The classic build

For pages that cannot use modules — an Ionic shell, for instance, where the script sits in the app template.

<script src="https://cdn.saphere.ai/saphere-scan/v1/js/main.min.js"></script>

The bundle sets window.Handler and then dispatches a handler-ready event on window.

Read the global, or wait for the event — not one or the other

window.Handler is only defined once the script has finished executing, and handler-ready is dispatched once, at that moment. A plain <script> tag runs before the code below it, so the global is already there and a listener added afterwards never fires. A script loaded with async, injected at runtime or loaded by a framework may run later, and then the global is not there yet. Handle both cases:

const mount = Handler => Handler.load("#container", options)

if (window.Handler)
    mount(window.Handler)
else
    window.addEventListener("handler-ready", ({ detail: { Handler } }) => mount(Handler), { once: true })

The event carries Handler in its detail, so the second branch never needs the global.

The Handler API

Everything is static. There is one widget per page.

Handler.load(target, options)

Mounts the widget. Returns nothing.

ParameterAccepts
targetA CSS selector string, or an HTMLDivElement
optionsThe options object — see Options

The container’s contents are replaced. Give it a height, because the widget fills it.

Throws:

MessageCause
SaphereScan is already loaded.A widget is already mounted. Call destroy() first, or use reload().
Module must be loaded in an HTMLDivElementThe selector matched nothing, or matched something that is not a div.
Bad integrator module options!Your options failed validation. The details are logged to the console.
Bad module options!The merged options failed validation: createMeasure missing or incomplete, an unknown top-level key, or a customisation conflicting with your options.

Read the console when options are refused

Both option errors log the full validation output before throwing. The thrown message alone does not say which field was wrong; the console entry does.

Handler.destroy()

Asynchronous. Unmounts the widget, empties the container and releases the camera.

await Handler.destroy()

Safe to call when nothing is mounted — it returns immediately.

Handler.reload(target, options)

Asynchronous. Equivalent to await destroy() then load(...). This is how you restart a measurement, or apply new options.

Getters

GetterTypeValue
Handler.isLoadedbooleanWhether a widget is currently mounted
Handler.langsstring[]["ar","de","en","es","fr","it","pl","pt","pt_BR","tr"]
Handler.defaultModuleOptionsobjectThe defaults, before your options are merged
Handler.isMobilebooleanDevice detection — the widget’s own guess, which an isMobile option overrides inside the widget

Handler.isMobile is useful for layout: on a desktop browser you usually want to constrain the widget to a phone-shaped column rather than let it fill a wide viewport.

A complete page

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0">
    <title>Saphere Scan v1</title>
    <style>
        html, body { padding: 0; margin: 0; overflow: hidden; }
        #container { height: 100vh; }
        /* On a desktop browser, keep the widget in a phone-shaped column. */
        #container:not(.mobile) { aspect-ratio: 9 / 16; margin: auto; }
    </style>
</head>
<body>
    <div id="container"></div>

    <script type="module">
        import { Handler } from "https://cdn.saphere.ai/saphere-scan/v1/mjs/main.min.js"

        const container = document.getElementById("container")

        const options = {
            createMeasure: {
                // Your server creates the measure and returns { id }.
                // Never put an API key in a page — see the warning below.
                strategy: "delegate",
                url: "/api/saphere-measure"
            },
            onEvent: event => {
                console.log(event.type, event)
                if (event.type === "result")
                    console.log("variables:", event.variables)
            },
            allowLeave: true,
            lang: "en"
        }

        if (Handler.isMobile)
            container.classList.add("mobile")

        Handler.load(container, options)
    </script>
</body>
</html>

The API key does not belong in the page

Published v1 examples show headers: { Authorization: "Bearer <apiKey>" } written directly into the page. That places a credential which opens your entire account, and which cannot be revoked without deactivating the account, into anything that can read your JavaScript.

Point createMeasure.url at an endpoint on your own server, authenticate your user there, and have that endpoint call the Saphere API with the key. See Options for both strategies.

Serve the page over HTTPS or from localhost. Browsers refuse camera access on an insecure origin.