Guides
Troubleshooting
Every error is returned as an RFC 9457 problem document with
Content-Type: application/problem+json. Match on the code field. The
detail text is for people and may change within a contract version.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"detail": "The nonce was already used in this environment.",
"code": "error.nonce_reused",
"instance": "/api/v1/authorize"
}
The Node SDK raises GrayPassApiError with the same code and status. The
full list of codes is in the reason code reference.
Credentials and environments
401 with no code, or error.not_found on a resource you just created.
The Bearer credential is missing, revoked, or belongs to another tenant.
Resources owned by another tenant answer error.not_found, never
error.caller_not_allowed, so the response cannot be used to discover ids.
Check that the credential is active on the Console Integrations page.
error.environment_mismatch. A credential is bound to one environment by
its mode: a sandbox credential cannot read or change the live environment,
through the API or the Console. Use a credential minted for the environment
you are addressing, or, in the Console, switch the environment selector.
error.browser_credential_not_allowed. A browser client token was used
where a server credential is required. Browser tokens can submit evidence,
read their own session status, and complete a challenge. Authorization,
subject, action, and proof operations need the sk_ credential on your
backend.
error.caller_not_allowed. The credential is not permitted to authorize
this action. Either the active policy lists allowed_callers and this
credential is not among them, or a challenge or review is being completed by
a credential other than the one that made the original request.
error.rate_limited. The caller exceeded the bounded request rate for
this surface. Back off and retry with the same idempotency key.
Registering actions and policies
error.context_invalid on registration. The context schema uses a keyword
outside the supported subset, a float, an undeclared required property, or
nesting deeper than twelve levels. The supported keywords are type (object,
string, integer, boolean, array, null), properties, required,
additionalProperties, enum, minLength, maxLength, minimum,
maximum, items, title, and description. Anything else is rejected at
registration so you never rely on a rule that is not enforced. See
Protected actions and policies.
error.policy_inactive. The action has no active policy version. Create a
version and activate it before authorizing. Activation retires the previous
version in the same transaction.
error.policy_conflict. A concurrent change to the action or its
policy raced with your request. Re-read the action and retry.
error.policy_integrity. The stored policy content no longer matches its
recorded digest. Authorization fails closed. This should never occur in normal
operation; report it with the action id.
error.enforcement_not_approved. You asked a live action to enter enforce
mode. Live enforcement is blocked by the server until an applicable
real-human validation and calibration release is registered. Sandbox actions
can enter enforce mode for integration testing.
Subjects and enrollment
error.subject_not_found. The subject id does not exist in this
environment. Subjects are environment-scoped; a sandbox subject is not visible
from live.
error.subject_not_enrolled. Authorization or a challenge was requested
for a subject whose enrollment has not completed. Wait for
status().enrollment_ready in the browser, then call completeEnrollment from
the backend.
Enrollment never becomes ready. The server counts only accepted evidence
windows. Each acknowledgement carries coverage, evidence_windows,
active_seconds, and modalities_present. Rejected windows carry one of
evidence.rejected_unsigned (the frame signature was missing or invalid, so
the browser SDK is not receiving its frame signing key), evidence.rejected_replay
(the window repeated or overlapped counted evidence), or evidence.rejected_quality
(the window failed integrity checks). Elapsed time never counts as coverage.
The demo or SDK reports "Evidence transport paused". The browser could not reach the evidence endpoint or the session ended. Confirm the page origin is registered on the Console Integrations page and that the client token has not expired.
Optional camera
Camera permission was declined. Continue with the person's other permitted interaction evidence. Show an enable control if they choose to try again; never repeatedly prompt. Camera refusal supplies no gaze evidence and no camera-related penalty.
The camera starts but its models fail to load. Check that assetBase
points to the page's own origin and includes the worker, models, and WASM
files. The page must permit a first-party worker, and the worker response
must carry the dedicated WASM Content Security Policy. Setting headers on
the remote API does not configure your application's worker. See
camera asset hosting.
Calibration finishes without becoming ready. Check the returned status
and local guidance, improve lighting and camera position, then let the
person choose to retry. Validation uses different targets from calibration.
Viewport or sustained head-position changes invalidate calibration; hiding
the page stops the camera. Returning or restarting requires explicit camera
enablement and fresh calibration. Poor camera measurements contribute no
gaze evidence.
Authorization requests
error.action_not_registered. The action key is not registered in this
environment, or it is archived (error.action_archived). Keys are exact and
case-sensitive.
error.audience_not_allowed. The request's audience is not among the
active policy's allowed_audiences. The audience is the relying party that
will verify the proof, and it is bound into the proof.
error.context_invalid on authorize. The request context does not match
the action's context schema. The detail names the failing property.
error.request_stale. The requested_at timestamp is outside the
accepted freshness window. Send the current time and check clock skew on your
servers.
error.nonce_reused. A nonce was reused within the environment. The SDK
derives a stable nonce from idempotencyKey; if you supply nonces yourself,
generate a fresh one per request and reuse it only for retries of the same
request.
error.idempotency_conflict. The idempotency key was reused with a
different subject, action, resource, context, audience, or nonce. Retries
must resend the same request. A different request needs a different key.
error.request_in_progress. Another request with the same idempotency key
is still being decided. Wait and retry; you will receive the stored result.
error.idempotency_stale. A stored idempotent result is no longer safe
because the subject, action, or policy changed after it was decided. Submit a
new request with a new key.
Every decision is challenge or hold with decision.insufficient_evidence.
The evidence does not meet the policy's coverage_min or freshness_max_ms.
Confirm an observer with purpose: 'observe' is running in the browser for
the same subject and session you pass to authorize, and inspect the
Assurance Profile in the Console activity trace.
Every decision is hold with operator_change.detected or
automation.detected. A detector fired. A challenge cannot clear it; the
request needs an attributed review. Scripted or replayed input in a test
harness commonly triggers automation.detected.
Challenges
error.challenge_expired. Challenges expire five minutes after the
decision. Request a new authorization.
error.challenge_mismatch. The challenge id belongs to a different
request, was already completed, or was completed with a method the policy
does not list. Each challenge completes once.
sandbox_confirm fails with error.environment_mismatch. The labeled
sandbox confirmation exists only in sandbox environments. Live challenges
accept passkey only.
Proofs
error.proof_unavailable. The signing service could not issue a proof.
The decision is still recorded; no proof was issued and nothing should
execute. Retry with the same idempotency key once the signer recovers.
verify() returns issuer_mismatch. The verifier's expected issuer does
not match the proof. In production the issuer is https://app.graypass.org.
Self-hosted deployments set GRAYPASS_PROOF_ISSUER and configure the SDK's
issuer to match. The Node SDK can resolve it from GET /api/v1/credential.
verify() returns audience_mismatch or action_hash_mismatch. The
proof was issued for a different relying party or a different request. Verify
with the audience your policy allows and the action_hash from the
authorization you retrieved, never with values copied out of the proof.
verify() returns unknown_kid. The proof's kid is not in the
published JWKS. Refresh the key set from
/.well-known/graypass-proof-keys.json; keys rotate with an overlap window.
verify() returns not_enforceable. The proof was issued outside enforce
mode or carries a non-human actor. Shadow and test decisions never carry an
enforceable proof.
verify() returns replayed. Your replay store has already seen this
jti. Each proof is accepted once per relying party.
A proof verifies offline but the status endpoint reports consumed.
Offline signature validity and online state are different. One-time
consumption is a scoped conditional update; a proof consumed once is not
accepted again even though its signature remains valid until expiry.
Webhooks
Registration fails with webhook_url_unsafe. Private, loopback,
link-local, and unresolvable destinations are refused at registration and at
every delivery attempt. Use a public HTTPS endpoint.
A test event reports dns_failure or a refused destination. The
destination did not resolve or was refused before the first attempt, so
nothing was parked. Fix the destination and send another test event.
Signature verification fails. Verify with parseWebhook from
@graypass/node against the raw request body, the X-GrayPass-Timestamp,
and the X-GrayPass-Delivery-Id. After rotating a secret, deliveries and
replays sign with the new secret immediately.
Events stop arriving. Deliveries that exhaust their retries are parked as dead letters on the Console Integrations page, where they can be replayed or discarded. A webhook bound to one environment never receives the other environment's events.
Getting help
Include the instance, the code, the trace_id of the authorization when
there is one, and the environment. Write to
tools@graypass.org. Never include a raw proof or
a credential in a report; the trace id is enough.
Generated from the API contract in contracts/ and the source in docs/. The build fails if this page disagrees with the running API.