API reference
challenge
/v1/challengeIssue or complete a friction-based challenge
Three distinct steps. (1) When friction is warranted: verify returns decision: "challenge" (or degraded); your app returns HTTP 428. That is not this endpoint. (2) Issue (no proof): create friction for that eventId / session. (3) Complete (challengeId + proof): prove the issued friction was satisfied; clears only the bound session, once. Issuing alone never clears a session.
Invariant (bound-session clear): Completing a challenge never globally clears an identity, account, IP, or device. It clears only its boundTo session, and only once (409 on replay).
Invariant (complete ≠ authorize): Completing a challenge does not itself authorize the action. It only changes session state. Re-verify with context.challengeId and proceed only on non-degraded allow. The original decision engine remains authoritative.
Chitmark decides when friction is warranted. You choose which mechanism fits the funnel via prefer: ordered hints, not guarantees. First issuable method wins; unsupported entries are skipped. Prefer payment_preauth → proof_of_work → device_attestation → web_bot_auth_stepup over email_otp; floor is proof_of_work, never email_otp.
Make challenge a configurable escalation policy: start with the least costly friction you already use (verified email, delayed credit release, and similar) and measure completion and downstream conversion by challenge type.
`payment_preauth`: Payment challenges validate the supplied payment-provider result (e.g. SetupIntent succeeded); they do not transfer funds or authorize a purchase. Chitmark is not a payments product.
Friction should be cheap for one real buyer and expensive at farm scale. The economic tax scales with risk via context.riskTier on issue: default → difficulty D (4), elevated → ≥ D (5), severe → ≥ elevated (6). Monotonicity: riskTier ↑ ⇒ challenge cost never ↓. Issued difficulty is immutable: sealed at issue; completion validates that value only. A later tier change cannot retroactively alter a challenge's required proof.
Headers
Bearer API key
application/json
Request body
From verify
Binding id you supply for one action attempt (prefer high-entropy, server-generated; max 128). Same value across issue, complete, and re-verify. Client-generated/guessable values weaken binding.
Ordered hints, not guarantees: first issuable method wins; unsupported entries skipped. Floor proof_of_work; never email_otp. Branch on returned method.
Completion mode: the challenge being solved
Completion mode: PoW nonce, or stripe_setup_intent setupIntentId (validates provider result only; no fund transfer or purchase auth)
Issue mode: riskTier scales PoW difficulty monotonically (default → D, elevated → ≥ D, severe → ≥ elevated). Sealed at issue; not re-applied on complete.
Response
Challenge identifier
Selected method
Method-specific solve parameters. For proof_of_work: required { type: "pow", difficulty, seed, algorithm: "sha256-prefix" } (client must use issued difficulty; sealed at issue).
Session the solve clears. Invariant: completion never clears identity, account, IP, or device; only this session, once.
Expiry
Completion mode only: true when cleared
Completion mode only: when the session cleared
Notes
- Issue: 400 bad request (including missing required
session), 401 unauthorized, 403 missingchallenge:write, 413 payload too large, 429 rate limited, 503 challenge store unavailable. - Complete: 400 invalid proof / method mismatch / payment not succeeded, 403 session mismatch, 404 unknown challenge, 409 replay, 410 expired, 413 payload too large, 429 rate limited, 503 unavailable.
- After completing, re-verify with
context.challengeIdso the decision engine remains authoritative. When honored, the verdict is always non-degradedallowwithchallengeCleared: true(clear short-circuits scoring; the flag never appears on challenge/deny). Completion alone never authorizes the action.
curl -s https://api.chitmark.com/v1/challenge \
-H "Authorization: Bearer $CHITMARK_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "eventId": "evt_2f9c…", "session": "sess_9f3a", "prefer": ["payment_preauth", "proof_of_work", "web_bot_auth_stepup"]}'{
"challengeId": "chl_88ab…",
"method": "proof_of_work",
"instructions": {
"type": "pow",
"difficulty": 4,
"seed": "…",
"algorithm": "sha256-prefix"
},
"boundTo": "sess_9f3a",
"expiresAt": "2026-03-15T12:05:00Z"
}