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.

JSON
{
  "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.