Skip to content
Transport modes · Direct

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.

Background-Check, not Passport

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.

server (Node)ts
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:

HTTP — challengehttp
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"}
HTTP — verifyhttp
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.