Skip to content
Shield · Error codes

Every error the Shield API returns

RootHerald returns string error values, never numeric codes. The body is always { error: string, message?: string } and the HTTP status carries the class. Copy the row into your error handler.

Error shape

Every non-2xx response from the Shield API has the same JSON shape. There is no code, requestId, or policyId field — just an error string and an optional human-readable message:

{
  "error": "challenge_expired_or_used",
  "message": "..."
}

Branch on the error string, not the wording of message. The HTTP status tells you the class at a glance: 400 bad request, 401 auth, 409 conflict, 422 unprocessable, 429 quota, 500 server-side.

Errors

errorHTTPMeaningWhat to do
invalid_secret_key401The challenge or verify call arrived without a valid rh_sk_ secret key in the Authorization header.Send `Authorization: Bearer rh_sk_...` from your backend. Never call these endpoints from the browser or with a publishable rh_pk_ key.
invalid_evidence400The verify body was malformed — missing challengeId, missing evidence, or the evidence carried no device id.Check the request shape against the API reference. Pass the challengeId you got from POST /attestations/challenge and the full evidence blob the client collected.
challenge_expired_or_used409The challenge id was unparseable, unknown to this tenant, or has already been consumed. Challenges are single-use and live ~5 minutes.Mint a fresh challenge with POST /attestations/challenge and re-run the client attestation. Do not reuse a challengeId across verify calls.
unknown_policy422The policy id named in the verify request does not exist, was soft-deleted, or belongs to another tenant. RootHerald fails closed — it never silently substitutes a different policy.Pass a valid built-in policy id (e.g. `rootherald:builtin:strict-hardware`) or one you created via POST /api/v1/admin/policies, or omit `policy` to use your tenant default.
quota_exceeded429The tenant hit its plan quota (free-tier monthly limit or a device account's one-time lifetime grant). The challenge is NOT consumed, so you can retry within quota.Upgrade the plan on the billing page. The response carries a quota header (`free-monthly-exceeded` or `device-lifetime-exceeded`) and a human-readable `message`.
invalid_api_key401An enroll or activate call (POST /api/v1/devices/enroll, /activate — the host-only leg) arrived without a valid secret key.These are server→server host relay endpoints; send a valid rh_sk_ secret key. If you're a customer integrating verify, you don't call these directly — the native host does.
metering_unavailable500The verify call reached a billable route with no resolvable tenant context, or usage metering threw. RootHerald fails closed rather than serving an unmetered attestation.Transient / server-side. Retry with backoff; if it persists, capture the request and contact support.

A fail verdict is not an error

A device that is cryptographically genuine but doesn't satisfy the active policy — a replayed or bad PCR quote, evidence that can't be bound to the chain-validated AK, a class the policy rejects, or a banned device — is not a 4xx. Those return HTTP 200 with a normal verify response whose verdict.device.verdict is "fail" (or "warn") and an earStatus of "contraindicated". Read the verdict fields, don't catch an exception. See the verdict-fields reference.

There is no policy_rejected, invalid_action, or banned error code — policy and ban outcomes are expressed in the verdict, not as transport errors.

EK / enroll validation failures (free-text 400)

The host-only enroll leg (POST /api/v1/devices/enroll) validates the TPM Endorsement Key certificate chain and the Attestation Key template. When that validation fails, the endpoint returns HTTP 400 with a free-text { "error": "<reason>" } — the human-readable rejection reason (e.g. "AK public area is not valid base64" or an EK-chain message), not a stable machine code. Treat these as diagnostic strings for the enrolling host, not values to branch on.

Internally, EK-chain rejections are categorised by the ChainFailureKind enum in the verifier. These are internal appraisal reasons, NOT wire error codes — they never appear as an error value on the response; they only shape the free-text reason and server-side logs:

  • EkKeyMismatch — the EK cert's public key doesn't match the EK public area the device presented (CVE-2021-3406 class).
  • EkNotTrusted — the chain built but didn't terminate at an embedded vendor root. Almost always a fake or non-spec TPM.
  • EkChainIncomplete — a complete chain to a trusted root couldn't be assembled and no fetch failed; the chain is genuinely missing intermediates.
  • EkChainFetchFailed — an AIA fetch to the vendor's PKI timed out / 404'd / TLS-failed. Operational, not cryptographic — a retry may succeed.
  • EkCertMissing — no EK certificate was supplied when one was required (capturing it is an elevated step on Windows).
  • Other — a chain-build error outside the categories above.