NeccessoryNeccessory
REST API

Submitting Measurements

POST a completed measurement to the SDK API, and GET it back by id.

This endpoint is for the on-device tier. There the SDK runs the whole pipeline locally, so there is no session to open and no video to upload: when a measurement completes, you submit its result to a single endpoint, which stores it and attributes it to your key.

Cloud measurements are not submitted

In the cloud tier the metrics are computed on our servers and the measurement is written before you ever see it. There is nothing to POST. Cloud results carry origin: "cloud" and attested: true; results submitted here carry origin: "on_device" and attested: false, because the client computed them — see Attestation.

All SDK-facing endpoints are under /sdk/v1 and require HMAC authentication. Responses use the standard envelope:

{ "ok": true, "data": { } }
{ "ok": false, "error": { "code": "string_snake", "message": "human readable" } }

Submit a measurement

POST /sdk/v1/measurements

Persists a completed measurement and attributes it to the key. The request body is the measurement result shape; source is required.

Body (abbreviated — see the full measurement result):

{
  "source": "web",
  "durationSec": 32.4,
  "isComplete": true,
  "sdkVersion": "0.1.0",
  "modelsVersion": "wv-5",
  "quality": { "level": "good", "score": 0.82, "usable": true },
  "cardiac": { "heartRate": { "value": 72, "unit": "bpm", "confidence": 0.88 } },
  "hrv": { "sdnnMs": 48, "rmssdMs": 41, "pnn50Pct": 12.5 },
  "respiratory": { "breathingRate": { "value": 15, "unit": "brpm" } },
  "stress": { "index": 34, "level": "low", "unit": "index" },
  "bloodPressure": { "systolicMmHg": 118, "diastolicMmHg": 77, "unit": "mmHg", "confidence": 0.51, "tier": "beta" },
  "spo2": { "value": 97, "unit": "%", "confidence": 0.60, "tier": "beta" }
}

Response — the stored measurement, with an assigned id:

{ "ok": true, "data": { "id": "meas_9c1f4b7ae2d05138b6a37f42", "keyId": "...", "quality": { "...": "..." }, "cardiac": { "...": "..." } } }
const res = await fetch('https://neccessory.com/sdk/v1/measurements', {
  method: 'POST',
  headers: signHeaders(keyId, secret),
  body: JSON.stringify(result),   // the completed measurement
});
const { data } = await res.json();

Naming the engine that produced the result

Two optional body fields say which build made the measurement:

FieldWhat it is
sdkVersionVersion of the client SDK that produced the result.
modelsVersionVersion of the model bundle it ran — for the web SDK, the identifier of the engine assets it loaded.

Both are strings of 1–64 characters matching ^[A-Za-z0-9][A-Za-z0-9._+-]*$ ("0.1.0", "wv-5", "2026-08-01+web"). A value outside that shape fails the whole request with 400 validation_error — it is never silently dropped. Both fields are optional: a request without them is stored exactly as before, so clients built against the earlier contract keep working unchanged.

They are part of the signed body. The signature covers a SHA-256 of the body you send, so add the fields before signing; nothing about the canonical string changes — see Authentication.

The web SDK does this for you. @neccessory/web-sdk stamps its own SDK_VERSION on every result it maps and every body it builds, and the on-device driver reports the model bundle it actually loaded (exported as ON_DEVICE_MODELS_VERSION), so submit() and signAndSubmit() already carry both:

import { createMeasurement, SDK_VERSION } from '@neccessory/web-sdk';

const m = createMeasurement({ videoEl, modelGrant });
m.init({ keyId, secret });
m.onResult(async (r) => {
  console.log(r.sdkVersion, r.modelsVersion);  // "0.1.0" "wv-5"
  await m.submit(r);                           // both travel inside the signed body
});

Set the fields yourself — on the result before submitting, or straight in the body — when you wrap the SDK in your own client or run your own capture stack; an explicit value wins over the built-in one. Cloud measurements need nothing: we computed them, so we already know the versions.

The stored measurement returns them back on GET, and the console groups on-device volume and quality by them.

Opting into raw data

By default the response omits the raw block (RR intervals and pulse signal). To persist and return it, opt in with either the query string or the body:

POST /sdk/v1/measurements?advanced=true
{ "source": "web", "includeAdvanced": true, "raw": { "rrIntervalsMs": [...], "ppgSignal": [...], "sampleRateHz": 30 } }

Read a measurement back

GET /sdk/v1/measurements/{id}

Returns one stored measurement. The record belongs to the key that stored it: signing with another key of the same account returns a plain 404, so an id is never a hint that the row exists. The query flags match the POST — advanced=true adds the raw block if it was persisted, include=assessment adds the wellness assessment.

GET /sdk/v1/measurements/meas_9c1f4b7ae2d05138b6a37f42?include=assessment

The signature covers the path without the query string and an empty body — see Authentication.

import { signAndFetchMeasurement } from '@neccessory/web-sdk';

const measurement = await signAndFetchMeasurement({
  keyId,
  secret,
  measurementId: 'meas_9c1f4b7ae2d05138b6a37f42',
  includeAssessment: true,
});

The same call by hand:

const path = `/sdk/v1/measurements/${id}`;
const res = await fetch(`https://neccessory.com${path}`, {
  headers: signHeaders(keyId, secret, { method: 'GET', path, body: '' }),
});
const { data } = await res.json();

The secret belongs on a server

Reading a measurement needs the key secret, exactly like submitting one. Call this from your backend, not from a page you ship to end users.

Verify credentials

POST /sdk/v1/auth/test verifies your HMAC signature and returns the key's facts — id, status, mode, features, limits, quotas, token balance, organization and server clock. A quick check that signing works. See Authentication.

Public discovery endpoints

No authentication required:

EndpointPurpose
GET /healthLiveness — { ok: true, version }.
GET /openapi.jsonOpenAPI 3.1 specification of the API.
GET /.well-known/agent.jsonAgent manifest (capabilities, auth).
GET /api/v1/pricingToken rates, metric sets and the purchasable packs — see Pricing.
GET /api/v1/limitsThe server's own limit constants, so you need not hardcode ours: keyRateLimitPerMinMax, orgWebhookLimit, webhookAttempts, measurementBaseDurationSec, cloudMinDurationSec, cloudMaxDurationSec, exportRowCap, maxWindowDays.

The rest of /api/v1 is the console's own API. It authenticates with a console session, not with an API key, so a nk_live_ key cannot reach it and it is not documented here.

Typical flow

  1. Run the on-device measurement (Web or iOS SDK).
  2. On completion, POST /sdk/v1/measurements with the result, HMAC-signed.
  3. Read the stored record back with GET /sdk/v1/measurements/{id}, or from your account.
  4. When something goes wrong, look the call up by its X-Request-Id in the usage log.

origin and attested are set by the server and cannot be supplied by a client. If you send them they are ignored — a submitted measurement is always on_device and never attested.

For the cloud endpoints — POST /sdk/v1/cloud/sessions, the upload presign, and the batch job endpoints — see Cloud quickstart and Batch processing.

Next: Errors.