Cloud Quickstart
Mint a session on your backend, hand the token to the page, and take a cloud measurement.
A cloud measurement takes two moving parts: your backend mints a short-lived session with your key, and your frontend runs the SDK with the token it was given. The secret never reaches the browser.
your backend ──NCS1──> POST /sdk/v1/cloud/sessions
← data = { sessionId, sessionToken, wsUrl, frameSpec, … }
│
└── the whole `data` ──> your page ──WSS──> face crops ──> live events ──> result1. Mint a session on your backend
POST /sdk/v1/cloud/sessions, signed with the same
NCS1 HMAC scheme as every other /sdk/v1 call.
The key must be active.
// request
{
"platform": "web", // "web" | "ios" | "android"
"durationSec": 60, // 20 .. 180, default 60
"features": ["hrv", "bp", "spo2"], // a subset; core metrics are always on
"consent": true, // REQUIRED — see below
"clientRef": "user-42" // optional, ≤ 64 chars, your own correlation id
}// 201
{
"ok": true,
"data": {
"sessionId": "cj_…",
"sessionToken": "<payload>.<signature>",
"wsUrl": "wss://infer.neccessory.com/v1/stream",
"expiresAt": 1785000000000,
"maxDurationSec": 60, // echoes the durationSec you asked for
"features": ["hrv", "bp", "spo2"], // the sets this session will compute and bill
"frameSpec": {
"kind": "face-crop",
"width": 36, "height": 36, "channels": 3,
"layout": "nhwc", "dtype": "uint8", "colorSpace": "srgb",
"padRatio": 0,
"nominalRateHz": 30, "maxRateHz": 32,
"compression": "none"
}
}
}maxDurationSec is the duration this session was actually granted, not the
tier ceiling: ask for 60 and you get 60 back. The ceiling — 180 s — only appears
as the upper bound of invalid_duration.
import crypto from 'node:crypto';
const sha256 = (v) => crypto.createHash('sha256').update(v).digest('hex');
function signHeaders(keyId, secret, method, path, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(12).toString('base64url');
const canonical = ['NCS1', method.toUpperCase(), path, timestamp, nonce, sha256(body)].join('\n');
return {
'x-key-id': keyId,
'x-timestamp': timestamp,
'x-nonce': nonce,
'x-signature': crypto.createHmac('sha256', sha256(secret)).update(canonical).digest('base64'),
'content-type': 'application/json',
};
}
const path = '/sdk/v1/cloud/sessions';
const body = JSON.stringify({
platform: 'web',
durationSec: 60,
features: ['hrv', 'bp', 'spo2'],
consent: true,
});
const res = await fetch(`https://neccessory.com${path}`, {
method: 'POST',
headers: signHeaders(process.env.NC_KEY_ID, process.env.NC_SECRET, 'POST', path, body),
body,
});
const { data } = await res.json();
// Return the whole `data` object to the page. It is all public: the token, the
// socket address, the session id and the frame spec. Your secret is not in it.
return Response.json(data);Hand the page the whole `data` object
The session token alone is not enough to run a measurement. The SDK also needs
wsUrl to know where to stream and frameSpec to crop exactly the way the
server expects, and neither is recoverable from the token. Forward only
sessionToken and the measurement fails with no_ws_url before the camera even
opens — in every delivery mode except batch.
(sessionId and the granted duration are readable from the token's claims, so
the SDK falls back to those. wsUrl and frameSpec have no such fallback.)
Nothing in data is a credential beyond the session itself: the token is scoped
to one session, is single-use, and expires in 300 seconds.
`consent: true` is mandatory
The cloud tier transmits images of the user's face. A request without
consent: true is rejected with 400. Collecting and recording that consent is
your responsibility as the integrator; the API only refuses to proceed without
your assertion that you have it. See Cloud privacy.
About the token
- It is
base64url(payload).base64url(signature)— a signed statement of session id, key id, organization, platform, allowed duration, and features. - It is short-lived: 300 seconds by default. Mint it when the user is about to measure, not when the page loads.
- It is single-use: a second connection with the same session id is rejected
with
session_already_used. - It is safe to give to the browser. It is not the secret and it cannot be used to sign anything else.
2. Run the measurement in the browser
Fetch the session object from your own endpoint and pass it straight through as
session. The SDK reads sessionToken, sessionId, wsUrl, maxDurationSec
and frameSpec out of it — you never unpack them yourself.
<video id="cam" autoplay playsinline muted width="480"></video>
<p>Heart rate: <span id="hr">--</span> bpm</p>
<p id="status"></p>
<button id="start">Start</button>
<script type="module">
import { createMeasurement } from '@neccessory/web-sdk';
// The whole `data` object from POST /sdk/v1/cloud/sessions, forwarded verbatim
// by your backend: { sessionId, sessionToken, wsUrl, maxDurationSec, frameSpec }.
const session = await fetch('/your-backend/nc-session', { method: 'POST' })
.then((r) => r.json());
const m = createMeasurement({
videoEl: document.getElementById('cam'),
mode: 'cloud',
session,
delivery: 'auto',
});
m.onRealtime((update) => {
if (update.heartRate != null) {
document.getElementById('hr').textContent = update.heartRate;
}
});
m.onStage((stage) => {
// 'requestingCamera' | 'loadingModels' | 'connecting' | 'ready'
// and, when streaming falls back to batch, 'uploading' then 'processing'.
document.getElementById('status').textContent = stage;
});
m.onResult((result) => {
// result.origin === 'cloud', result.attested === true
console.log(result);
});
m.onError((err) => console.error(err.code, err.message));
document.getElementById('start').addEventListener('click', () => m.start());
</script>If you would rather keep the pieces apart — for example because your endpoint
returns them under different names — pass sessionToken, sessionId and wsUrl
as individual options instead. Each one overrides the matching field of
session. Omitting wsUrl is only safe with delivery: 'batch'; anything else
fails with no_ws_url.
There is no init({ keyId, secret }) in cloud mode and no model grant: the token
carries the authorization, and there are no model weights to download.
3. Read the result
The result is the same measurement result you get on-device, with two extra fields:
{
"origin": "cloud",
"attested": true,
"quality": { "level": "good", "score": 0.82, "usable": true }
// … all the usual sections
}The measurement is already stored under your key by the time you see it —
the inference service writes it. You do not call
POST /sdk/v1/measurements for a cloud measurement; that endpoint is for
on-device results. See Attestation.
What can go wrong
| Situation | What you see |
|---|---|
| Token older than its TTL | token_expired |
| Token reused | session_already_used |
Page got sessionToken but no wsUrl | no_ws_url before the camera opens (any delivery but batch) |
NC_INFERENCE_WS_URL not configured on our side | 503 cloud_unavailable at mint time |
| More than 32 frames per second | frame_rate_exceeded |
| No packets for 10 seconds | idle_timeout |
| Signal too weak for a real answer | A final with quality.level: "poor", isComplete: false, all metrics null |
The last row is deliberate. neccessory does not fabricate numbers: a poor capture returns "no measurement", not a plausible-looking one.
The complete list is in Limits & errors.
Next: Streaming protocol.