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.
| Build | URL | How you get Handler |
|---|---|---|
| Classic script | https://cdn.saphere.ai/saphere-scan/v1/js/main.min.js | window.Handler, or the handler-ready event |
| ES module | https://cdn.saphere.ai/saphere-scan/v1/mjs/main.min.js | import { 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 URLs above are Production. Only the host changes between environments; the paths are identical:
| Environment | CDN base |
|---|---|
| Production | https://cdn.saphere.ai |
| Test | https://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.
Wait for the event, do not poll
window.Handler is only defined once the script has finished executing. A script tag lower in the page that reads it immediately may run first.
window.addEventListener("handler-ready", ({ detail: { Handler } }) => {
Handler.load("#container", options)
})
The event carries Handler in its detail, so you never need to touch the global at all.
The Handler API
Everything is static. There is one widget per page.
Handler.load(target, options)
Mounts the widget. Returns nothing.
| Parameter | Accepts |
|---|---|
target | A CSS selector string, or an HTMLDivElement |
options | The options object — see Options |
The container’s contents are replaced. Give it a height, because the widget fills it.
Throws:
| Message | Cause |
|---|---|
SaphereScan is already loaded. | A widget is already mounted. Call destroy() first, or use reload(). |
Module must be loaded in an HTMLDivElement | The 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 — usually a customisation conflicting with yours. |
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
| Getter | Type | Value |
|---|---|---|
Handler.isLoaded | boolean | Whether a widget is currently mounted |
Handler.langs | string[] | ["ar","de","en","es","fr","it","pl","pt","pt_BR","tr"] |
Handler.defaultModuleOptions | object | The defaults, before your options are merged |
Handler.isMobile | boolean | Device detection, as the widget itself sees it |
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.