Direct — your server calls api.rootherald.io
The default for the Background-Check flow. Your server calls https://api.rootherald.io server→server with your rh_sk_ secret key. The user's device never holds a key and never talks to Root Herald; it just hands an opaque evidence blob to your backend.
In the default model the client is dumb: it collects an opaque evidence blob (no keys, no Root Herald contact) and posts it to yourserver. Your server is the only thing that talks to Root Herald: it relays a nonce to the client, then submits the evidence for appraisal with its secret key and gets the verdict back directly. “Direct” here means your server reaches api.rootherald.io with no extra DNS or proxy hop, not that the client does.
What you do
Construct the server client with your secret key. The API base is baked in; you only override it for the custom-domain or proxy transports.
import { RootHerald } from "@rootherald/node";
// Direct mode: base URL defaults to https://api.rootherald.io.
const rh = new RootHerald({ secretKey: process.env.RH_SECRET_KEY! }); // rh_sk_…
// 1. Mint a nonce and relay it to your client.
const { challengeId, nonce } = await rh.issueChallenge();
// 2. Your client quotes over `nonce` and posts the opaque `evidence` back to you.
// Submit it server→server for appraisal:
const verdict = await rh.verify(evidence, {
challengeId,
policy: "rootherald:builtin:strict-hardware",
});
if (verdict.device.verdict === "pass") {
// verdict.device.ueid is your stable per-tenant device id.
}What it looks like on the wire
Two server→server calls, both authenticated with your rh_sk_ secret key. First the challenge, then the verify:
POST /api/v1/attestations/challenge HTTP/1.1
Host: api.rootherald.io
Authorization: Bearer rh_sk_live_…
Content-Type: application/json
{}
HTTP/1.1 200 OK
{"challengeId":"chl_…","nonce":"b64…","expiresAt":"2026-06-18T18:09:11Z"}POST /api/v1/attestations/verify HTTP/1.1
Host: api.rootherald.io
Authorization: Bearer rh_sk_live_…
Content-Type: application/json
{"challengeId":"chl_…","evidence":{ /* opaque blob from the client */ },"policy":"rootherald:builtin:strict-hardware","returnToken":false}
HTTP/1.1 200 OK
{"verdict":{ /* AttestationVerdict */ }}An un-enrolled or failing device is not an error: verify returns 200 with a fail (or warn) verdict. Only protocol/auth/quota problems raise an error: 401 (bad/missing secret key), 422 (unknown/foreign policy name), 409 (challenge spent/expired), 400 (malformed evidence), 429 (quota).
Edge geography
Direct mode terminates at the nearest of our edge POPs (us-west-2, us-east-1, eu-west-1 in beta; eu-central-1, ap-southeast-2 in Wave 2). Because the call is server→server, the latency that matters is from yourbackend, not the end user's device. The appraisal path is edge → regional verifier → response, target p95 280ms.
When to switch
You only need to switch off direct mode if one of the following is true:
- Your security review prohibits a third-party domain in your server's egress path.
- You want your domain (not
rootherald.io) to appear in server-side network requests. - You have a residency requirement that can't be satisfied by our edge geography.
The first two cases push you to custom domain. The third pushes you to customer proxy.