Guides
Decisions and challenges
Every authorization request produces exactly one decision for one registered
action: allow, challenge, hold, or deny. The decision arrives with the
Assurance Profile it was made from, the reason codes that explain it, and, for
allow in enforce mode, a Proof of Agency. Your application enforces the
decision; GrayPass never executes anything.
The four decisions
| Decision | Meaning | What your application does |
|---|---|---|
allow |
The active policy is satisfied by the current evidence. | In enforce mode, verify the proof where the action runs, execute, then report the outcome. |
challenge |
A requirement is unmet and the policy names a safe, action-bound recovery. | Show the exact action and ask the person to approve it with a listed method. |
hold |
A hold condition matched, or a positive detector fired. | Queue the request for an attributed manual review. Nothing executes until a reviewer approves it. |
deny |
A deny condition matched. | Refuse the action. A challenge cannot recover a denial. |
The decision is shown to people in plain words: allow as "Approved",
challenge as "Verify again", hold as "Paused for review", and deny as
"Blocked". Your application may use its own wording. The response carries the
phrases in human_readable.
How a decision is reached
The policy version active for the action is evaluated in a fixed order. The first branch that applies produces the decision.
- Deny conditions. Any reason code listed in the policy's
deny_onpresent in the profile producesdenywithdecision.deny_condition. - Hold conditions. Any code listed in
hold_onproducesholdwithdecision.hold_condition.operator_change.detectedandautomation.detectedproduce a hold even when a policy omits them, so a positive signal never turns into a recoverable challenge because coverage was also thin. - Evidence sufficiency. Coverage below
coverage_min, freshness older thanfreshness_max_ms, or aninsufficient_evidencecontinuity status produce the decision named in the policy'sinsufficient_evidencefield:challenge,hold, ordeny. The reason codes carrydecision.insufficient_evidenceplus the specific shortfall. - Per-claim requirements. Each claim in
requirements.claimsmust reach its minimum status. Detection claims (operator_change,automation) pass only in their exact good state. Ifexplicit_approval_requiredis set, the request also needs a completed action-bound approval. Any unmet requirement produceschallengewithdecision.requirement_unmetand the unmet codes. - Allow. Everything above passed:
decision.policy_satisfied, ordecision.challenge_completedwhen a challenge supplied the approval.
Shadow and test modes run the same evaluation and record the same decision,
but they never issue a proof and your application must not execute on them.
Their traces carry decision.shadow_mode or decision.test_mode.
Insufficient evidence is a status, not a decision
The Assurance Profile can report coverage.sparse, freshness.stale, or
operator_continuity.insufficient_evidence. These describe the evidence. The
policy decides what they mean for this action. A payout change might route
thin evidence to a challenge; an administrative privilege change might hold it
for review. Neither path can map insufficient evidence to allow.
Challenges
A challenge decision includes a challenge object with an id, the methods the
policy permits, and an expiry five minutes after the decision. Two methods
exist:
passkey: an action-bound WebAuthn ceremony with user verification. The challenge carries an immutable display snapshot (action, consequence, resource digest, context digest, requesting application). The browser SDK shows it and asks the person to confirm the exact action before the passkey is used. The completed approval is recorded asapproval_method.passkey.sandbox_confirm: a labeled confirmation available only in sandbox environments. It exists so an integration can exercise the full flow without a passkey. It is recorded asapproval_method.sandbox_confirmand is never represented as phishing-resistant approval.
Completing a challenge re-runs the original authorization request as a child
authorization with explicit_action_approval.confirmed added to the profile.
The approval compensates only for the shortfalls listed in the policy's
challenge_compensates (for example operator_continuity, coverage, or
freshness). It never compensates for:
- a firing detector (
operator_change.detected,automation.detected); - continuity that was positively contradicted
(
operator_continuity.not_confirmed); - any deny condition.
The child authorization is the one that carries the proof. Retrieve it by id,
recompute the action hash from your own request material, verify the proof,
execute, and report the outcome. Completing a challenge twice, after expiry,
or against a different request fails with error.challenge_mismatch or
error.challenge_expired.
Holds and review
A held authorization waits for an attributed decision by a reviewer with the
security reviewer, admin, or owner role in the Console. Approval re-runs the
original request as a child authorization under the credential that made it,
with the approval recorded as decision.manual_review_approved and
approval_method.manual_review. In enforce mode that child carries the proof
your application verifies. The behavioral limitations in the profile remain
unchanged. A reviewer's approval is authoritative over missing or stale
evidence, but not over a live attack signal: if a policy deny condition or a
hard indicator (automation.detected, operator_change.detected,
operator_continuity.not_confirmed) is firing when the approval is
processed - including one that fired after the hold was created - the fresh
evaluation stands, the attempt is recorded as
decision.manual_review_superseded, and no enforcement proof is issued.
Rejection records decision.manual_review_rejected and the outcome
manually_rejected. If the action's policy or mode changed while the request
was held, the review fails with error.policy_conflict and the request stays
pending.
Outcomes
Report what your application actually did after each decision with one
immutable outcome per authorization: executed, not_executed,
manually_approved, manually_rejected, confirmed_attack,
false_challenge, customer_override, or unknown. Repeating the same
report is idempotent; a contradicting second report is rejected. Outcomes are
the labels shadow evaluations are measured against. They are supplied by you
and are not verified ground truth.
Reading a response
{
"id": "auth_...",
"decision": "challenge",
"mode": "enforce",
"reason_codes": ["decision.requirement_unmet", "explicit_action_approval.not_present"],
"human_readable": { "status": "Verify again", "detail": "Approve this exact action to continue." },
"assurance": {
"coverage": "sufficient",
"freshness_ms": 780,
"claims": [
{ "claim": "operator_continuity", "status": "likely", "coverage": "sufficient", "reason_codes": ["operator_continuity.likely"] },
{ "claim": "operator_change", "status": "not_detected", "coverage": "sufficient", "reason_codes": ["operator_change.not_detected"] },
{ "claim": "automation", "status": "not_detected", "coverage": "sufficient", "reason_codes": ["automation.not_detected"] },
{ "claim": "human_presence", "status": "unsupported", "coverage": "none", "reason_codes": ["human_presence.unsupported"] }
],
"limitations": ["limitation.enrollment_unanchored", "limitation.experimental_assessment", "limitation.no_calibrated_confidence"]
},
"challenge": { "id": "chl_...", "methods": ["passkey"], "expires_at": "..." },
"proof": null
}
Match on decision and reason_codes. The human-readable strings are for
people and may change wording within a contract version. Every code is listed
in the reason code reference.
Generated from the API contract in contracts/ and the source in docs/. The build fails if this page disagrees with the running API.