Verify a device, end to end
Prove a request comes from a real, distinct physical device — not a bot, a script, or a rented cloud server — before you trust it. This verification is one round trip with two halves: your backend orchestrates and holds the key; a keyless clienton the user's machine collects the hardware evidence. This page sets up the server half — the part that ships today — then hands you off to a client guide for the platform you target.
The integration shape
One concept: your backend holds the only key (rh_sk_…) and does the appraisal; the client just collects an opaque evidence blob over a fresh nonce. The blob carries no verdict and the client never talks to Root Herald. Four beats, interleaving server and client:
- serverYour backend mints a single-use challenge —
rh.issueChallenge()→{ challengeId, nonce }. - relayYour backend hands the
nonceto your client. - clientThe client quotes the TPM over that nonce and returns an opaque
evidenceblob. It holds no Root Herald key. - serverYour backend submits it —
rh.verify(evidence, { challengeId })→ verdict — and you act onverdict.device.verdict/verdict.device.ueid.
Set up the server half (available today)
In the dashboard, create a project and copy its secret key (rh_sk_…, server-side only — the single key kind Root Herald issues). Then add the server SDK, which is live on npm:
npm install @rootherald/nodeThese two calls are the server bookends around any client. issueChallenge() opens the round trip; verify() closes it and returns the verdict directly — there is no token for you to check.
import { RootHerald, QuotaExceededError } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…
// Beat 1 — mint a challenge; relay { challengeId, nonce } to your client.
app.post("/api/challenge", async (_req, res) => {
const { challengeId, nonce } = await rh.issueChallenge();
res.json({ challengeId, nonce });
});
// Beat 4 — your client posted back { challengeId, evidence }; appraise it.
app.post("/api/verify", async (req, res) => {
const { challengeId, evidence } = req.body;
let verdict;
try {
verdict = await rh.verify(evidence, {
challengeId,
policy: "rootherald:builtin:strict-hardware",
});
} catch (err) {
// verify() throws only on protocol/auth/quota problems — a failing device
// is a normal verdict, not an exception.
if (err instanceof QuotaExceededError) return res.status(429).end();
throw err;
}
if (verdict.device.verdict !== "pass") {
return res.status(403).json({ error: "device_check_failed" });
}
// verdict.device.ueid is a stable per-tenant device pseudonym — use it for
// one-device-one-account, rate limits, or a ban list.
res.json({ ok: true, deviceId: verdict.device.ueid });
});rh.verify() returns a typed AttestationVerdict: branch on verdict.device.verdict (pass / warn / fail), read verdict.device.ueid for one-device-one-X enforcement, and check verdict.acr for the assurance level. Full field reference: Verdict, field by field.
Pick how your client collects evidence
The server half is identical everywhere. What changes is where beat 3 runs — in a web page or inside a native binary. Pick your track; each guide walks the full client + server flow end to end.