Quickstart
Init the Web SDK with your key, start a measurement, and read the result.
This page covers the on-device tier. For the cloud tier — where your backend mints a session token and the metrics are computed on our servers — see Cloud quickstart.
Minimal measurement
The browser needs only your key id to run a measurement. The secret never leaves your server: the page forwards the result to your backend, which signs and submits it.
<video id="cam" autoplay playsinline muted width="480"></video>
<p>Heart rate: <span id="hr">--</span> bpm</p>
<button id="start">Start</button>
<script type="module">
import { createMeasurement } from '@neccessory/web-sdk';
const m = createMeasurement({
videoEl: document.getElementById('cam'),
});
m.init({
keyId: 'nk_live_sRwG9z2hK2hdV4Il83J7Cw', // safe to embed; the secret is not
});
m.onRealtime((update) => {
// Live during capture: current heart rate + quality level only.
if (update.heartRate != null) {
document.getElementById('hr').textContent = update.heartRate;
}
});
m.onResult(async (result) => {
// Once, on completion — the full measurement result.
await fetch('/your-backend/measurements', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
});
});
m.onError((err) => console.error(err.code, err.message));
document.getElementById('start').addEventListener('click', () => m.start());
</script>Keep your face in view and hold still. A live heart rate appears after about 10
seconds; the full measurement completes in roughly 30–35 seconds and arrives via
onResult.
Your backend then signs the forwarded result with the secret and posts it to
/sdk/v1/measurements — see Authentication for the
canonical string and the NCS1 headers.
Submitting straight from the browser
m.submit(result) signs and submits without a backend, but it needs the secret in
init() — which publishes it to everyone who opens your page. Use it only from a
server runtime, or from a first-party page whose visitors you already trust with
the key.
m.init({ keyId: 'nk_live_sRwG9z2hK2hdV4Il83J7Cw', secret: '…' });
m.onResult(async (result) => { await m.submit(result); });A secret in the browser is a public secret
Anyone who opens devtools on your page can copy it and sign requests as you:
submit fabricated measurements into your account, burn your rate limit, and pull
model artifacts. There is no origin allowlist to fall back on — a signed request
is accepted from anywhere, and Origin is a header only browsers bother to
send. If the page is public, forward through your backend instead: mint a
cloud session server-side, or submit the result
from your own server.
The lifecycle
| Method | Effect |
|---|---|
createMeasurement({ videoEl }) | Create the measurement and bind it to the video element to read from. Optional previewEl and overlayEl canvases render the preview and face overlay. |
m.init({ keyId, secret }) | Attach your key. Synchronous — it stores credentials, it does not open the camera. Omit secret if your backend submits results. |
await m.start() | Load the engine, open the camera, and begin the measurement. |
await m.stop() | Stop the current measurement and release the camera. |
await m.submit(result) | HMAC-sign the result and submit it. Requires a secret. |
start() reports failures through onError rather than rejecting, so a single
error path covers both startup and capture.
Callbacks
| Callback | Fires | Payload |
|---|---|---|
onRealtime(cb) | ~1/second during capture | { heartRate, quality: { level, score } } — live values only. |
onResult(cb) | Once, on completion | The full measurement result. |
onError(cb) | On any error | { code, message } — no internal details. |
See the Callbacks reference for exact payload shapes.
Next: Callbacks reference.