API reference

challenge

POST/v1/challenge

Issue 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

Authorizationstringrequired

Bearer API key

Content-Typestringrequired

application/json

Request body

eventIdstringrequired

From verify

sessionstringrequired

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.

preferChallengeMethod[]optional

Ordered hints, not guarantees: first issuable method wins; unsupported entries skipped. Floor proof_of_work; never email_otp. Branch on returned method.

challengeIdstringoptional

Completion mode: the challenge being solved

proofobjectoptional

Completion mode: PoW nonce, or stripe_setup_intent setupIntentId (validates provider result only; no fund transfer or purchase auth)

contextobjectoptional

Issue mode: riskTier scales PoW difficulty monotonically (default → D, elevated → ≥ D, severe → ≥ elevated). Sealed at issue; not re-applied on complete.

Response

challengeIdstringrequired

Challenge identifier

methodChallengeMethodrequired

Selected method

instructionsobjectrequired

Method-specific solve parameters. For proof_of_work: required { type: "pow", difficulty, seed, algorithm: "sha256-prefix" } (client must use issued difficulty; sealed at issue).

boundTostringrequired

Session the solve clears. Invariant: completion never clears identity, account, IP, or device; only this session, once.

expiresAtdate-timerequired

Expiry

okbooleanoptional

Completion mode only: true when cleared

clearedAtdate-timeoptional

Completion mode only: when the session cleared

Notes

  • Issue: 400 bad request (including missing required session), 401 unauthorized, 403 missing challenge: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.challengeId so the decision engine remains authoritative. When honored, the verdict is always non-degraded allow with challengeCleared: true (clear short-circuits scoring; the flag never appears on challenge/deny). Completion alone never authorizes the action.
Request
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"]}'
Response
{
  "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"
}