API reference
verify
/v1/verifyReturn an action decision
Sub-50 ms at the edge. Returns allow | challenge | deny with principal resolution, confidence, reasons, verdictToken, and eventId (store for feedback).
On degradation, decision is always challenge. Prefer hashed subject fields (piiMode: hashed); raw email/IP are opt-in only.
Verify returns a three-way decision (allow ← challenge → deny), not a single risk threshold. Uncertainty sits in the middle; context.decisionCosts moves both boundaries with relative, dimensionless weights (not dollars): falseAllow = abusive traffic allowed, falseChallenge = legitimate traffic challenged when allow would have been fine, falseDeny = legitimate traffic denied. Defaults 1:1:3. See the outcome loop guide for the diagram.
Headers
Bearer API key
Dedupes retries of the same attempt. Conflict (409) when this key is already associated with a different payload fingerprint (action/session/surface/subject/context). Create a new key for a new request.
application/json
Request body
signup | trial_activation | api_key | demo_request | quote | ticket | custom (or any string). The current focus is signup and trial/API-key issuance.
email / emailSha256, ip / ipTrunc, userAgent, headers, formData / formDataHashed
Binding id you supply for this action attempt. Prefer high-entropy, server-generated ids (binds challenge and verdictToken)
e.g. app.acme.com/signup
Body form of Idempotency-Key; header takes precedence if both are sent
Arbitrary metadata (plan, referrer, …). logOnly: true is an integration-side control that emits an observation-only log_only verdict (nothing blocked or challenged): set it in your server code, never from client request data. The response reports the effective mode as enforcementMode. See the log-only guide. decisionCosts ({falseAllow, falseChallenge, falseDeny}) are relative, dimensionless weights (not dollars, not score thresholds; only ratios matter): falseAllow = cost of allowing abusive traffic; falseChallenge = cost of challenging legitimate traffic when allow would have been fine; falseDeny = cost of denying legitimate traffic. Higher falseAllow challenges earlier, higher falseChallenge lets more through, higher falseDeny reserves deny for confirmed abuse. Defaults 1/1/3; invalid fields fall back to defaults.
Response
Routing verdict (not HTTP status). Partner gates map challenge → 428, deny → 403, allow → continue.
human | agent_principal | agent_abusive | unknown
Model confidence for the returned decision (0..1). Not a calibrated probability of abuse, and not the threshold decisionCosts moves. Gate on decision; use for ops triage and dashboards, not as a substitute allow/deny cutoff.
Human-readable signal strings for ops (always populated). Display copy today (string[]); do not scrape or regex the text as a machine API. A future additive revision may expose {code, message, category} (keeping a human-readable message) so analytics can group without parsing free text.
Attribution object (resolved, operator, onBehalfOf, identityBasis). Not authz: resolved is not "this is a human" and never unlocks an action alone. Gate on decision. See SDK.md §5 for field semantics (onBehalfOf may be org or end-user id; WBA primarily authenticates the operator).
ES256 JWT decision receipt. Claims: iss (always https://api.chitmark.com), tid, sub, aud, jti, iat/exp, plus decision fields. Validate signature (JWKS), then iss/tid/sub, before trusting. Under enforcementMode: "log_only" it is an observation receipt only.
Store on account: feedback join key
Operational flag, not a fourth decision. If true, decision is always challenge (timeout/error path).
Effective mode on the verdict (server-computed). log_only when env/key policy or integration-set context.logOnly applied; never a client-supplied field. Not a JWT claim: pair with verdictToken verification before authorizing.
Notes
- Three HTTP 200 Verdict shapes side-by-side: explicit
allow, scoredchallenge(degraded: false), and store-outagechallenge(degraded: true). Same schema; gate ondecision+degraded. - Error responses: 400 (bad request), 401 (unauthorized), 403 (forbidden: raw PII or missing
verify:write), 409 (idempotency conflict), 413 (payload too large), 429 (rate limited). Verify declares no HTTP 503. - Store outages return HTTP 200 with a Verdict body (
degraded: true,decision: "challenge"), never HTTP 503. Official SDKs deserialize that 200 as a Verdict. Handle scored and degraded challenges the same way: partner HTTP 428, thenPOST /v1/challenge. Transport timeout / unexpected 5xx: SDK synthesizes a local degraded challenge Verdict (or your integration maps to 428), never allow.
curl -s https://api.chitmark.com/v1/verify \
-H "Authorization: Bearer $CHITMARK_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: signup:sess_9f3a" \
-d '{ "idempotencyKey": "signup:sess_9f3a", "action": "signup", "session": "sess_9f3a", "surface": "app.acme.com/signup", "subject": { "emailSha256": "9c1185a5c5e9fc…", "ipTrunc": "203.0.113.0/24", "userAgent": "Mozilla/5.0 …", "headers": { "signature-agent": "\"https://sigdir.openai.com\"" } }, "context": { "plan": "free" }}'{
"decision": "allow",
"actorType": "agent_principal",
"confidence": 0.91,
"reasons": [
"Valid Ed25519 Web Bot Auth signature (operator=openai)"
],
"principal": {
"resolved": true,
"operator": "openai",
"onBehalfOf": "acmecorp.com",
"identityBasis": ["web_bot_auth", "email_domain"]
},
"verdictToken": "eyJhbGciOi…",
"eventId": "evt_2f9c…",
"degraded": false,
"enforcementMode": "enforce"
}