API reference

verify

POST/v1/verify

Return 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

Authorizationstringrequired

Bearer API key

Idempotency-Keystringoptional

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.

Content-Typestringrequired

application/json

Request body

actionstringrequired

signup | trial_activation | api_key | demo_request | quote | ticket | custom (or any string). The current focus is signup and trial/API-key issuance.

subjectobjectrequired

email / emailSha256, ip / ipTrunc, userAgent, headers, formData / formDataHashed

sessionstringoptional

Binding id you supply for this action attempt. Prefer high-entropy, server-generated ids (binds challenge and verdictToken)

surfacestringoptional

e.g. app.acme.com/signup

idempotencyKeystringoptional

Body form of Idempotency-Key; header takes precedence if both are sent

contextobjectoptional

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

decisionallow | challenge | denyrequired

Routing verdict (not HTTP status). Partner gates map challenge → 428, deny → 403, allow → continue.

actorTypeenumrequired

human | agent_principal | agent_abusive | unknown

confidencenumberrequired

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.

reasonsstring[]required

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.

principalobjectrequired

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).

verdictTokenstringrequired

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.

eventIdstringrequired

Store on account: feedback join key

degradedbooleanrequired

Operational flag, not a fourth decision. If true, decision is always challenge (timeout/error path).

enforcementMode"enforce" | "log_only"required

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, scored challenge (degraded: false), and store-outage challenge (degraded: true). Same schema; gate on decision + 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, then POST /v1/challenge. Transport timeout / unexpected 5xx: SDK synthesizes a local degraded challenge Verdict (or your integration maps to 428), never allow.
Request
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" }}'
Response
{
  "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"
}