Options
Everything Handler.load() accepts: the measure strategy, the event callback, the eleven-colour palette, the screens, and the texts.
New integrations should use Saphere Scan v2. What follows documents the v1 options object.
Your options are validated, then merged over the defaults — and over any customisation applied by ?customizationId=. Unknown top-level keys are rejected rather than ignored.
Top level
| Key | Type | Default | Notes |
|---|---|---|---|
createMeasure | object | — | Required. How a measure is obtained. |
onEvent | function | — | Called on every event. |
allowLeave | boolean | false | Shows a control letting the user quit the widget. |
allowSkip | boolean | false | Lets the user skip onboarding screens. |
lang | string | browser language, else "en" | One of the ten supported languages. |
useShadow | boolean | true | Mounts inside a shadow root. |
colors | object | see below | Eleven hex colours. |
texts | object | built-in | Per-language string overrides. |
pages | object | see below | Which screens appear, and how. |
desiredVariables | string[] | — | Narrows what is computed. |
tags | string[] | — | Stored with the measure. |
`useShadow: false` lets your CSS reach inside
The default mounts the widget in a shadow root, which stops your stylesheet from affecting it and its own from affecting you. Turning that off is occasionally necessary in hybrid shells; when you do, expect your global CSS to reach the widget’s internals.createMeasure
The widget needs a measure to attach the capture to. There are two strategies, and they differ in who makes the call.
delegate — the widget calls your endpoint
createMeasure: {
strategy: "delegate",
url: "/api/saphere-measure",
headers: { "X-Session": sessionId } // optional
}
The widget issues POST <url> with a JSON body carrying userData, desiredVariables and tags, and expects { "id": "<measureId>" } back.
Your endpoint authenticates your user, calls POST /measures on the Saphere API with your key, and relays the identifier.
handle — your code makes the call
createMeasure: {
strategy: "handle",
fetch: async ({ userData, desiredVariables }) => {
const response = await fetch("/api/saphere-measure", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userData, desiredVariables })
})
return await response.json() // must be { id: "…" }
}
}
`fetch` must resolve to an object with an `id`
Returning the identifier as a bare string throws Cannot get measureId!. The contract is { id: string } — an object.
This is worth checking first when a v1 integration fails at the very start: some published examples returned json["token"], which is a string, and does not satisfy it.
If the endpoint answers with a non-2xx status, the widget throws Cannot create measure!.
onEvent
onEvent: async event => {
console.log(event.type, event)
}
Called for every event below. The return value is meaningful: returning literally false cancels the transition the event announces. Anything else — including undefined, and including a thrown exception, which is logged and swallowed — allows it.
The handler is awaited.
A slow handler slows the widget
Because the return value can veto a transition, v1 waits for your handler before proceeding. A handler that performs a network call on every event delays the user journey by that call.
This is the behaviour v2 deliberately dropped: there, onEvent is never awaited, and vetoing moved to a separate hooks API with a bounded timeout.
The event union
type | Payload | When |
|---|---|---|
video-stream-loading | { status: "start" } | The camera is being requested |
video-stream-loading | { status: "ready" } | The stream is open |
video-stream-loading | { status: "error", reason } | The camera was refused |
start | — | The face is accepted and framing is frozen |
record | — | Capture begins |
end | — | All frames are buffered and the end marker is queued |
result | { id, userData?, variables } | The measurement completed |
aborted | { reasons } | The measurement stopped |
leave | — | The user asked to quit (requires allowLeave) |
transition | { from, to } | Declared, but never emitted |
video-stream-loading reason is one of "denied", "no-device", "not-supported", "already-used".
aborted reasons carries either a conformity code (CONFORMITY_POOR_LIGHT, CONFORMITY_MUCH_VARIATIONS, CONFORMITY_FACE_PRESENCE, CONFORMITY_LOW_FPS, CREDIT_EXCEEDED, MISSING_USER_DATA_*, …) or a failure reason (WIDGET_RESIZED, FOCUS_LOST, CANCELED_WHILE_CAPTURING, CREATE_MEASURE_ERROR, WS_CONNECTION_ERROR, VIDEO_SOURCE_ENDED, …).
`transition` never fires
TransitionEvent is part of the declared type, and a handler branching on it will compile. Nothing in the module constructs one. Do not build navigation tracking on it.In a result event, each variable is { value, signals, error: null } on success, or { value: null, error } on failure.
colors
Eleven slots, each a hex string (#RGB or #RRGGBB). All eleven are required once you supply the object.
| Key | Default | Role |
|---|---|---|
color01 | #002B49 | Primary colour |
color02 | #FF585D | Secondary colour |
color03 | #EDF1FF | Popup background |
color04 | #FFE055 | Star colour |
color05 | #002B49 | Primary button text |
color06 | #FF585D | Primary button background |
color07 | #002B49 | Secondary button text |
color08 | #EDF1FF | Secondary button background |
color09 | #FFFFFF | Module background |
color10 | #002B49 | Face detection — valid |
color11 | #FF585D | Face detection — invalid |
Only color01 and color02 are used to derive further tints and shades; the rest are applied literally.
pages
Which screens appear, and how.
pages: {
notice: { ignore: false },
logo: { ignore: false, data: null },
staticFacePlacement: { ignore: false },
result: { ignore: false, titles: { ignore: false } },
validates: {
1: { ignore: false }, 2: { ignore: false },
3: { ignore: true }, 4: { ignore: false },
5: { ignore: false }, 6: { ignore: false },
7: { ignore: false }, 8: { ignore: false }
}
}
| Key | Notes |
|---|---|
notice | Shows the notice button |
logo | { ignore: true }, or { ignore: false, data } where data is an image source. null shows the built-in i-Virtual logo. |
staticFacePlacement | The static face guide during placement |
result | The result screen; titles.ignore hides its headings |
The eight onboarding screens
All eight are required when you supply validates.
| Screen | Content | Default |
|---|---|---|
1 | Introduction | shown |
2 | Data-protection notice | shown |
3 | User-data form | hidden |
4 | Guidance — posture | shown |
5 | Guidance — uncovered face (mobile) | shown |
6 | Guidance — lighting | shown |
7 | Guidance — stillness | shown |
8 | Guidance — final | shown |
Screen 3 takes a fields object when enabled:
validates: {
3: {
ignore: false,
fields: {
height: { ignore: false },
weight: { ignore: false },
age: { ignore: false },
sex: { ignore: false },
smokingStatus: { ignore: false },
externalId: { ignore: true }
}
}
}
All six keys are required, and they cannot all be ignore: true — a form with no fields is refused.
Removing guidance costs measurements
Screens 4 to 7 are what tell users to sit still, uncover their face and avoid backlighting. Removing them shortens the journey and lowers the proportion of measurements that produce a usable signal.texts
Every user-visible string, overridable per language. The object must carry all ten language keys — ar, de, en, es, fr, it, pl, pt, pt_BR, tr — and each is merged over the built-in strings, so you override only what you name.
texts: {
...Handler.defaultModuleOptions.texts,
en: {
...Handler.defaultModuleOptions.texts.en,
button: { next: "Continue" }
}
}
Spreading the defaults is the practical way to satisfy the requirement without restating ten languages by hand.