# Verification model
> **TARGET architecture — implementation pending.** This model supersedes the
> scaffold's `valid` boolean and combined failure reasons once implemented.
## Two stages, two questions
Verification and assessment deliberately answer different questions:
1. **Provider verification:** did the configured provider product accept the
opaque token, and what normalized evidence did it return?
2. **Application assessment:** given that verdict, does the configured policy
permit this request?
`Captcha::verify` returns a `ProviderVerdict`. `Captcha::assess` returns an
`Assessment` containing the same provider verdict/evidence plus a `Decision`.
The evidence remains inspectable even when policy denies the request.
| Provider accepts and policy passes | `Ok(Assessment { Accepted, Allow })` |
| Provider accepts but score is too low | `Ok(Assessment { Accepted, Deny(ScoreTooLow) })` |
| Provider accepts but required supported evidence is absent | `Ok(Assessment { Accepted, Deny(MissingEvidence) })` |
| Provider rejects invalid/expired/reused token | `Ok(Assessment { Rejected, Deny(..) })` |
| Configuration is incompatible | `Err(CaptchaError::Configuration(..))` at construction |
| Transport, timeout, provider outage, or protocol failure | `Err(CaptchaError::Verification(..))` |
A rejected verdict always leads to `Decision::Deny`; policy failures may be
empty because the provider rejection itself is sufficient. The assessment API
must not relabel provider rejection as `PolicyFailure`.
## Invariants
- `ProviderVerdict::Accepted` cannot carry rejection reasons.
- `ProviderVerdict::Rejected` must carry a `ProviderRejection` and may carry
optional safe evidence returned with the rejection.
- `Decision::Allow` cannot carry policy failures.
- `Decision::Deny` may carry one or more policy failures when policy evaluation
caused denial.
- All evidence is attributable to exactly one `ProviderId` and configured
verifier instance.
- A request's `CaptchaToken` and provider credentials never appear in a verdict,
evidence, assessment, error, serialization, or debug output.
- `VerificationRequest` contains inputs sent for verification; policy
expectations live only in `VerificationPolicy`.
## Policy evaluation
`VerificationPolicy` may express expected action, allowed hostnames, minimum
score, and maximum challenge age. Construction checks requirements against the
verifier's effective `Capabilities`:
- an unsupported requirement fails construction with `CaptchaError` wrapping
`ConfigurationError::UnsupportedCapability`;
- supported evidence absent on a concrete accepted verdict denies with
`PolicyFailure::MissingEvidence(Capability)`;
- present evidence that violates its requirement gets the specific failure,
such as `ScoreTooLow` or `ActionMismatch`.
Missing evidence never silently passes. Challenge-age calculation uses an
injected/testable clock and documents clock-skew treatment during
implementation.
## Attempts and fallback
A verification attempt is bound to one adapter instance. No orchestrator may
submit the token to a different provider as fallback: provider tokens are not
portable. An application that offers multiple CAPTCHA products must route each
token to the verifier selected before challenge issuance.
Same-provider retry is also conservative. Never retry a provider rejection.
Retry an indeterminate network attempt only when the adapter can establish that
the operation is safe under that provider's single-use and idempotency rules.
Turnstile's idempotency mechanism can make a retry safe when the same key is
preserved; a generic retry policy cannot assume this for every provider. The
default is no retry; opt-in policy, cancellation, token-state, and budget rules
are authoritative in [Attempts and safe retries](attempts-and-retries.md).
Provider documentation may recommend application-level fail-open behavior
during an outage. That recommendation never changes SDK semantics: an
indeterminate verification returns `CaptchaError::Verification`, never an
accepted verdict or `Decision::Allow`. The application alone owns any explicit
outage policy outside this SDK assessment. Error categories and provider-code
mapping are defined in the [error taxonomy](errors.md).