The /v1/decide API
Full request/response schema, authentication, and error handling.
Endpoint
POST https://api.kaizocore.com/v1/decideAuthentication
Every request must be HMAC-signed. Four headers are required:
| Header | Meaning |
|---|---|
X-KZ-Key | Your pk_live_... or sk_live_... key |
X-KZ-Ts | Unix timestamp (seconds) — requests outside a ±5 minute window are rejected |
X-KZ-Rid | A unique request ID — reused IDs are rejected as replays |
X-KZ-Sig | HMAC-SHA256 signature, hex-encoded |
Signing string (newline-joined, in this exact order):
METHOD
PATH
TIMESTAMP
SHA256(body)
REQUEST_IDHMAC key: if you're signing with sk_live_... (your backend secret
key), the key is the secret key itself. If you're signing with
pk_live_... (safe to embed client-side — this is what the collector
script uses), the key is SHA256(your public key) — this lets the SDK sign
requests without ever needing your server secret, while still getting body
integrity and replay protection.
# Example: signing a request with sk_live_
BODY='{"ip":"203.0.113.7","session_token":"st_abc123","event_type":"checkout"}'
TS=$(date +%s)
RID=$(uuidgen)
BODY_HASH=$(echo -n "$BODY" | shasum -a 256 | awk '{print $1}')
SIGNING_STRING="POST
/v1/decide
$TS
$BODY_HASH
$RID"
SIG=$(echo -n "$SIGNING_STRING" | openssl dgst -sha256 -mac HMAC -macopt hexkey:$(echo -n "$SK_LIVE_KEY" | xxd -p -c 256) | awk '{print $2}')
curl -X POST https://api.kaizocore.com/v1/decide \
-H "Content-Type: application/json" \
-H "X-KZ-Key: $SK_LIVE_KEY" \
-H "X-KZ-Ts: $TS" \
-H "X-KZ-Rid: $RID" \
-H "X-KZ-Sig: $SIG" \
-d "$BODY"Request body
{
"ip": "203.0.113.7",
"user_agent": "Mozilla/5.0 ...",
"url": "https://yourapp.com/checkout",
"referrer": "https://yourapp.com/cart",
"session_token": "st_abc123...",
"entity_keys": { "user_id": "42", "email": "user@example.com" },
"headers": { "x-kz-ja3": "..." },
"event_type": "checkout",
"timestamp": 1752480000
}| Field | Type | Required | Notes |
|---|---|---|---|
ip | string | Recommended | Used for IP enrichment, velocity, and impossible-travel checks. Omitting it doesn't error, but disables everything IP-derived. |
user_agent | string | Recommended | Feeds device-layer rules (known-bot UA fragments, etc). |
url | string | Optional | Current page URL — used for journey confidence. |
referrer | string | Optional | Used for direct-hit detection. |
session_token | string | Effectively required | The token the collector SDK generated. Missing it returns SOFT_CHALLENGE immediately — see below. |
entity_keys | object | Optional | Any identity you want tracked for velocity/impossible-travel — user_id, email, anything meaningful to your flow. |
headers | object | Optional | Pass through raw headers like x-kz-ja3 (TLS fingerprint) if your infra has them available. |
event_type | string | Recommended | login, checkout, signup, payment, search — used for per-action thresholds. |
timestamp | int64 | Optional | Client-reported timestamp, informational. |
Missing session_token
If session_token is empty, KaizoCore returns SOFT_CHALLENGE immediately
without running the full scoring engine — this is a customer-integration
signal (the SDK isn't wired up correctly, or a legitimate non-JS client is
calling in), and the deliberate choice is to challenge, never hard-block,
since blocking here would drop real users behind a broken integration.
Response body
{
"request_id": "a6af22a9-3a6c-42e8-b28f-3e3184093d79",
"decision": "SOFT_CHALLENGE",
"score": 32.5,
"reasons": ["datacenter_ip", "missing_accept_headers"],
"review_hint": "",
"latency_ms": 187,
"session_mode": "FORGE:BOUND",
"shadow_routed": false
}| Field | Type | Notes |
|---|---|---|
request_id | string | Unique per call — use this to correlate with webhook deliveries and dashboard lookups. |
decision | string | One of ALLOW, ALLOW_WATCH, SOFT_CHALLENGE, REVIEW, BLOCK — see Decision scores. |
score | number | The fused 0–100 risk score. |
reasons | string[] | Every rule that fired, de-duplicated. Always an array, never null. |
review_hint | string | Optional guidance for manual review queues. |
latency_ms | int | Server-side processing time. |
session_mode | string | See Session modes. |
shadow_routed | bool | true if this session was additionally routed to the shadow review queue (MIRROR/GHOST). |
Rate limiting
Every response includes rate-limit headers:
X-KZ-Rl-Limit: 2000
X-KZ-Rl-Remaining: 1847
X-KZ-Rl-Reset: 1752480060A 429 includes Retry-After in seconds.
Error responses
| Status | Meaning |
|---|---|
400 | Malformed JSON body |
401 | Signature verification failed, or unknown API key |
413 | Body exceeds the 64 KB limit |
429 | Rate limit exceeded |
500 | Internal error — genuinely rare; the engine is built to degrade gracefully (e.g. ML models absent, IP enrichment failed) rather than error out |
What to do with the decision
| Decision | Suggested handling |
|---|---|
ALLOW | Proceed normally. |
ALLOW_WATCH | Proceed normally — this band is informational, for your own analytics. |
SOFT_CHALLENGE | Show a lightweight challenge (CAPTCHA, email OTP) before proceeding. |
REVIEW | Proceed, but flag for manual/async review — useful for e.g. holding an order for fraud review without blocking checkout outright. |
BLOCK | Deny the action. |
These are suggestions, not requirements — you're free to map decisions to your own UX. Per-action thresholds let you tune the score boundaries themselves per event type.