Skip to content
Shield · API reference

REST API reference

These are the enrollment + attestation endpoints. Most integrations never call them by hand. The standard path is the @rootherald/node server SDK, which wraps the Background-Check flow: POST /api/v1/attestations/challenge then POST /api/v1/attestations/verify, both with your rh_sk_ secret key, returning the verdict directly. (Verifying a JWT against our JWKS is only for the optional portable token or the badge tier.) Base URL is https://api.rootherald.ioby default; if you've enabled a custom domain or proxy transport, swap that in. The machine-authoritative contract is the live OpenAPI document below.

OpenAPI source

The live OpenAPI document is published at https://api.rootherald.io/api/v1/openapi.json (no authentication required). This page renders a static snapshot generated at build time so you get an indexable HTML version; the published JSON is always authoritative.

Authentication

There are two key types, and they authenticate in two different ways:

  • rh_sk_… secret key: server-side use only. Sent as a plain Authorization: Bearer rh_sk_…header (not OAuth) on the server→server Background-Check and admin calls.
  • rh_pk_… publishable key: safe in client code. Sent in the X-RootHerald-Site-Key header for client/site attribution and origin-pinning. It is never an Authorization: Bearertoken and can't perform server-side or admin operations.
curl example — mint a challenge (server→server)bash
curl -X POST "https://api.rootherald.io/api/v1/attestations/challenge" \
  -H "Authorization: Bearer rh_sk_live_2f9c4a1c…" \
  -H "Content-Type: application/json" \
  -d '{}'

Endpoints

POST/api/v1/attestations/challenge

Background-Check · mint a relay nonce (C1)

The default server→server flow. Your server calls this with its rh_sk_ secret key to mint a single-use nonce, relays the nonce to its dumb client (which quotes over it), and submits the result to /attestations/verify. No pre-enrolled RP, no client-side key.

Parameters

NameInTypeRequiredDescription
AuthorizationheaderstringyesBearer rh_sk_… secret key.
deviceHintbodystringnoAdvisory hint identifying the device; no pre-enrolled device required.

Response

200 with a single-use challenge (5-minute TTL). 401 on a missing/invalid secret key.

{
  "challengeId": "2f9c4a1c-8b…",
  "nonce": "base64…",
  "expiresAt": "2026-06-18T18:09:11Z"
}
POST/api/v1/attestations/verify

Background-Check · appraise evidence server→server (C2)

Submit the opaque evidence blob your client collected, against a named policy, and get the verdict back directly. An un-enrolled / failing device is NOT an error — it returns 200 with a fail (or warn) verdict. Set returnToken:true to also receive a signed EAT verifiable offline against our JWKS.

Parameters

NameInTypeRequiredDescription
AuthorizationheaderstringyesBearer rh_sk_… secret key.
challengeIdbodystringyesThe single-use challenge id from /attestations/challenge.
evidencebodyobjectyesOpaque evidence blob from the client collector; forwarded verbatim.
policybodystringnoTenant-owned policy id/name or a rootherald:builtin:* name. Unknown/foreign names fail closed (422). Omitted ⇒ strict-hardware default.
returnTokenbodybooleannoOpt into a signed EAT (JWT) in the response. Default false.

Response

200 with the verdict (and token when requested). 401 invalid key · 422 unknown_policy · 409 challenge_expired_or_used · 400 invalid_evidence · 429 quota_exceeded.

{
  "verdict": {
    "acr": "urn:rootherald:device:high",
    "device": {
      "ueid": "2f9c4a1c-8b…",
      "verdict": "pass",
      "earStatus": "affirming",
      "attestationType": "tpm20",
      "secureBootVerified": true,
      "trustworthinessVector": { "hardware": 2, "configuration": 2 }
    }
  },
  "token": null
}
POST/api/v1/devices/enroll

Enroll a device

First-contact enrollment of a TPM/Secure-Enclave device. The client submits its EK certificate and a TCG-standard AK template; the server validates the EK chain, runs make-credential, and registers a tenant-scoped device. Performed by the native host, not the relying party's server.

Parameters

NameInTypeRequiredDescription
AuthorizationheaderstringyesBearer rh_sk_… secret key.
ekPublicKeybodystringyesPEM/base64 EK public key produced by the client.
akPublicAreabodystringyesTCG-standard AK public area (TPM2B_PUBLIC), base64-encoded.
platformbodystringyesClient platform (e.g. "windows", "linux", "macos").
ekCertPembodystringnoPEM-encoded EK certificate. Omitted for firmware TPMs (Intel PTT / some AMD fTPM) with no NV-stored EK cert.
ekCertificateChainbodystring[]noPEM-encoded intermediate CA certs the device read from its own TPM NV; path-building material that must still terminate at a seeded root.

Response

200 with the make-credential challenge on a validated EK chain + AK template; 400 otherwise.

{
  "deviceId": "2f9c4a1c-8b…",
  "credentialBlob": "…",
  "encryptedSecret": "…"
}
POST/api/v1/devices/activate

Activate an enrolled device

Completes the activate-credential handshake: the client returns the decrypted make-credential secret, proving the AK is bound to the same TPM as the EK. Routed through PCP on Windows (raw activate is command-blocked).

Parameters

NameInTypeRequiredDescription
AuthorizationheaderstringyesBearer rh_sk_… secret key.
deviceIdbodystringyesDevice id returned by /devices/enroll.
decryptedSecretbodystringyesThe decrypted make-credential secret.
akPublicKeybodystringnoAK public key; used to bind quote verification to the activated AK.

Response

200 on a matching secret; 400 if activation fails.

{
  "deviceId": "2f9c4a1c-8b…",
  "status": "enrolled",
  "enrolledAt": "2026-06-18T18:09:11Z"
}

Rate limits

The API applies per-IP and per-session fixed-window rate limits to protect the service. When a limit is hit, the API returns HTTP 429 with a Retry-After header. Use exponential backoff; the SDKs implement this automatically. We do not publish a fixed per-second throughput SLA — if you have high-volume needs, talk to us.

Error response shape

error responsejson
{
  "error": "unknown_policy",
  "message": "No policy named 'strict-hardwar' is visible to this tenant"
}

Errors are string values, never numeric codes. The error field is always present; message is an optional human-readable detail. The HTTP status carries the class (400/401/409/422/429/500). Common values: invalid_secret_key, invalid_api_key, challenge_expired_or_used, invalid_evidence, unknown_policy, quota_exceeded, metering_unavailable.

Full error-code catalog lives at Shield · Error codes (Shield-specific) and wire-protocol error codes (transport-level).