Authentication
The single key model — key id + secret, signed with HMAC-SHA256 over the full request.
The SDK-facing REST API lives under /sdk/v1 on the base URL
https://neccessory.com. Every request is authenticated with your one key —
a public key id and a secret — using HMAC-SHA256.
Your key
Create one in Console → Keys. You receive a public key id (safe to embed and log) and a secret.
Both values carry a prefix that names the key's mode: nk_live_ on a live key
(cloud or on_device) and nk_test_ on a test key — for
example nk_live_sRwG9z2hK2hdV4Il83J7Cw. The prefix exists so a value spotted
in a log or a diff is recognizable at a glance; it is not part of the protocol
and carries no authority. Keys issued before the prefixes existed have none and
keep working unchanged — the server reads the mode from the key record, never
from the string, so do not validate a key id against a prefix in your own code.
The secret is shown once
We store only the signing key derived from your secret, so we cannot show it to
you again or recover it for you. Save it when it is created; if you lose it,
rotate the key to get a new one. There are no modules, field sets, or token
limits — one key unlocks the full metric set. A key does carry a mode
(cloud, on_device or test), which decides whether it may hand model
weights to a client and whether its results are real; signing is identical
either way. See Obtaining keys and
test keys below.
The same signature covers the cloud endpoints —
POST /sdk/v1/cloud/sessions, the upload presign, and the batch jobs. The
browser never signs anything in the cloud tier: your backend signs, and the page
receives a short-lived session token instead. See
Cloud quickstart.
Signing a request
Four headers authenticate each call:
| Header | Value |
|---|---|
x-key-id | Your public key id. |
x-timestamp | Current Unix time in seconds. |
x-nonce | A fresh random string, 8–128 chars, unique per request. |
x-signature | Base64( HMAC-SHA256( signingKey, canonicalString ) ). |
The signing key is SHA256(secret) as lowercase hex — never the secret
itself. The canonical string is these six lines joined with \n:
NCS1
<HTTP method, uppercase>
<request path, without query string>
<x-timestamp>
<x-nonce>
<SHA256(request body) as lowercase hex — SHA256("") for an empty body>The signature therefore covers the method, the path and the body, so a captured signature cannot be replayed against a different request. The server accepts a timestamp within a ±300 second window and rejects each nonce after its first use inside that window.
Where you may call from
/sdk/v1 answers any origin. A correctly signed request works from your
server, from a browser on your own domain, and from a terminal alike — there is
no origin allowlist to register.
The flip side is that the signature is the only thing standing between your account and anyone holding the secret. Keep it server-side wherever you can, and see the secret in a browser before you ship one to a public page.
Rate limits
Most /sdk/v1 paths carry no blanket rate limit, but three scoped mechanisms
exist: the streaming endpoints are limited per session, the key owner can enable
a per-key limit in the console, and a monthly quota of cloud seconds caps cloud
processing. All three are documented in
Cloud → Limits & errors. Treat 429 rate_limited as a
response your client must already handle (back off and retry).
Node.js
import crypto from 'node:crypto';
const sha256 = (value) => crypto.createHash('sha256').update(value).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');
const signature = crypto
.createHmac('sha256', sha256(secret))
.update(canonical)
.digest('base64');
return {
'x-key-id': keyId,
'x-timestamp': timestamp,
'x-nonce': nonce,
'x-signature': signature,
'content-type': 'application/json',
};
}
const res = await fetch('https://neccessory.com/sdk/v1/auth/test', {
method: 'POST',
headers: signHeaders(process.env.NC_KEY_ID, process.env.NC_SECRET, 'POST', '/sdk/v1/auth/test'),
});
const body = await res.json();
// { ok: true, data: { keyId, status, mode, features, rateLimitPerMin, ... } }Swift
import Foundation
import CryptoKit
func sha256Hex(_ value: String) -> String {
SHA256.hash(data: Data(value.utf8)).map { String(format: "%02x", $0) }.joined()
}
func signHeaders(
keyId: String,
secret: String,
method: String,
path: String,
body: String = ""
) -> [String: String] {
let timestamp = String(Int(Date().timeIntervalSince1970))
let nonce = Data((0..<12).map { _ in UInt8.random(in: .min ... .max) })
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
let canonical = [
"NCS1",
method.uppercased(),
path,
timestamp,
nonce,
sha256Hex(body),
].joined(separator: "\n")
let key = SymmetricKey(data: Data(sha256Hex(secret).utf8))
let mac = HMAC<SHA256>.authenticationCode(for: Data(canonical.utf8), using: key)
return [
"x-key-id": keyId,
"x-timestamp": timestamp,
"x-nonce": nonce,
"x-signature": Data(mac).base64EncodedString(),
"content-type": "application/json",
]
}Verify your key
POST /sdk/v1/auth/test confirms signing works and returns the key's facts — id,
status, mode, allowed features, the per-key rate limit (0 when none is set),
the monthly cloud-seconds and token quotas, the organization's token balance,
the organization id, and the server's clock in milliseconds, which is what you
compare against to catch clock skew:
{
"ok": true,
"data": {
"keyId": "nk_live_sRwG9z2hK2hdV4Il83J7Cw",
"status": "active",
"mode": "cloud",
"features": ["hrv", "bp"],
"rateLimitPerMin": 0,
"quotaSecondsMonth": null,
"tokenQuotaMonth": null,
"tokenBalance": 1250000,
"organizationId": "org_9f1c4b77a2e04d1385c6b0af",
"serverTime": 1756500000000
}
}Keep the secret private
Sign requests on your backend where you can, and never commit a secret to version control. If you cannot keep the secret out of a browser, submit measurements from your own backend instead of the browser. Rotate any leaked key immediately.
Test keys
A key created with mode test in the console behaves like a real key on the
wire and like a stub behind it. Its key id and secret carry the nk_test_
prefix instead of nk_live_, and requests are signed with the same NCS1
scheme — nothing in your signing code changes. What is different:
- It returns a deterministic synthetic result — no camera, no inference. The numbers are derived from the key id, so they are stable across runs.
- It bills nothing and meters nothing — test traffic never appears in usage.
- Every measurement it produces carries
"test": true. - Cloud sessions mint with
wsUrl: null, and the result matures after about 2 seconds — your client genuinely exercises its polling loop. - On-device models are not available: the model endpoints answer
403.
Test keys exist for your CI — end-to-end tests of signing, submission, polling
and parsing without a face in front of a camera. They are not for production.
Anything that verifies attestation must check both fields: attested: true
and the absence of test — see Attestation.
Next: Submitting measurements.