Verify a device in 5 minutes
Add hardware-rooted device verification to one action: a signup, a claim, a vote, a free-tier create. No user account, no CAPTCHA, no fingerprint script. The default model is RATS Background-Check: a dumb client collects an opaque evidence blob and hands it to your server; your server appraises it with Root Herald server→server and gets the verdict back directly. No client-side keys.
Create a project and grab your keys
In the dashboard, create a project. You get a publishable site key (safe in the browser) and a secret key (rh_sk_…, server-side only; this is what your backend uses to call Root Herald). New projects start on the rootherald:builtin:strict-hardware acceptance policy: hardware TPMs, firmware TPMs, and mobile hardware attestation only; cloud virtual TPMs and emulators are rejected. Change that any time from Customize → acceptance policies.
Mint a challenge and collect evidence
Your server mints a single-use challenge and relays the nonce to the client. The client (a dumb collector that holds no keys and never contacts Root Herald) quotes over that nonce and posts the resulting opaque evidence blob back to your backend with the rest of the form. On platforms with a native client (desktop, mobile) the collector talks to the OS attestation API; on the web it uses the Root Herald browser extension, falling back gracefully.
import { RootHerald } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…
app.post("/api/challenge", async (_req, res) => {
const { challengeId, nonce } = await rh.issueChallenge();
// Relay nonce + challengeId to the client; keep challengeId server-side too.
res.json({ challengeId, nonce });
});@rootherald/browser is in development. Today, collect evidence through the native client or the hosted attestation UI, and appraise it with the shipping @rootherald/node server SDK (Step 3).Appraise the evidence on your server
One server→server call with your secret key. rh.verify() returns the verdict directly — there is no token for you to verify. The verdict tells you whether this is a real, distinct, healthy device, and gives you a stable, tenant-scoped pseudonym you can use for one-device-one-X enforcement, rate limits, or a ban list.
import { RootHerald, QuotaExceededError } from "@rootherald/node";
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! });
app.post("/api/signup", async (req, res) => {
const { challengeId, evidence, email, password } = 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 — NOT on a failing
// device (that comes back as a normal verdict, below).
if (err instanceof QuotaExceededError) return res.status(429).end();
throw err;
}
// A real-but-rejected device returns a verdict; you decide what to do.
if (verdict.device.verdict !== "pass") {
return res.status(403).json({ error: "device_check_failed" });
}
// verdict.device.ueid is stable for this device in your tenant — use it to
// enforce "one free trial per device", to rate-limit, or to check a ban list.
if (await alreadySignedUp(verdict.device.ueid)) {
return res.status(409).json({ error: "device_already_registered" });
}
// ...create the account, store verdict.device.ueid against it...
res.json({ ok: true });
});The rh.verify() API
rh.verify(evidence, options) is the only appraisal call you need. It submits the opaque evidence blob to POST /api/v1/attestations/verify with your secret key and returns a typed AttestationVerdict. It does not throw on a failing device. That comes back as a normal verdict with device.verdict of fail / warn. It throws only on protocol/auth/quota problems (InvalidSecretKeyError 401, UnknownPolicyError 422, ChallengeError 409, InvalidEvidenceError 400, QuotaExceededError 429).
Options:
challengeId: the single-use challenge id fromrh.issueChallenge()(required).policy: a tenant-owned policy id/name or arootherald:builtin:*name. Unknown/foreign names fail closed (422).returnToken: opt into a portable signed EAT (JWT) you can verify offline withverifyAttestationToken(). Defaultfalse; under Background-Check you normally don't need it.
If you opt into a token (returnToken: true), or you're on the backend-less badge tier where a keyless SPA collects evidence and the RootHerald server — reached with your origin-pinned publishable rh_pk_ site key — returns the signed token (the client holds no secret and signs nothing), verify it offline with verifyAttestationToken(token, { issuer, audience })— it fetches the issuer's JWKS, checks the signature and iss/aud/expiry, and returns the same AttestationVerdict. See the Node SDK reference.
Reading the verdict
{
"acr": "urn:rootherald:device:high", // assurance level the device satisfied
"amr": ["hwk_tpm"], // RFC 8176 authentication methods
"device": {
"ueid": "2f9c4a…", // stable per-tenant device UUID (no PII)
"verdict": "pass", // pass | warn | fail
"earStatus": "affirming", // affirming | warning | contraindicated
"attestationType": "tpm20",
"secureBootVerified": true,
"trustworthinessVector": { "hardware": 2, "instance-identity": 2, "configuration": 2, "executables": 2 }
}
}device.verdict / device.earStatus. pass / affirmingmeans the device's class and state cleared your acceptance policy. warn / warningmeans it was accepted under a lenient policy (e.g. a cloud vTPM without cross-validation). Decide per action whether that's good enough. failmeans it didn't clear the bar; the call still returns a verdict so you can log and respond yourself.What device.ueid is. A stable per-tenant device pseudonym. Same device + same tenant → same id, forever. Different tenants get different ids for the same device, so nothing is cross-correlatable unless you opt into federated reputation. It is not reversible to a hardware identifier and contains no PII.