📖 SecureX API Guide

🛡 Getting Started

All API endpoints require authentication via X-API-Key header or Authorization: Bearer <token>. Base URL: https://securex.yourdomain.com

Authentication

curl -X POST https://securex.yourdomain.com/api/v1/auth/risk/assess \
  -H "X-API-Key: your-tenant-api-key" \
  -H "Content-Type: application/json" \
  -d '{"tenant":"acme_corp","event_type":"LOGIN",...}'

Rate Limits

HeaderDescription
X-RateLimit-LimitMax requests per minute for this tenant
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets

When rate limited, the API returns 429 Too Many Requests with a Retry-After header.

Error Codes

CodeMeaningCommon Causes
400Bad RequestMissing required fields, invalid JSON
401UnauthorizedMissing or invalid API key
403ForbiddenIP not whitelisted, tenant inactive
404Not FoundInvalid endpoint
422Validation ErrorField type/format errors (see response for details)
429Too Many RequestsRate limit exceeded
500Internal ErrorServer-side failure (retry with backoff)

POST Auth Risk Assessment

/api/v1/auth/risk/assess

Evaluates authentication events (login, signup) and returns a risk score with an actionable decision.

First-time Signup ALLOW

A brand new user registers from a home computer. No failed attempts. Standard browser. Known device/IP metadata omitted — engine auto-detects history.

curl -X POST https://securex.yourdomain.com/api/v1/auth/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "SIGNUP",
    "user_id": "usr_new_001",
    "ip_address": "203.0.113.50",
    "device": {
      "device_hash": "dev_new_abc",
      "browser": "Chrome 148",
      "os": "Windows 10"
    },
    "metadata": {}
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/auth/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "SIGNUP",
        "user_id": "usr_new_001",
        "ip_address": "203.0.113.50",
        "device": {
            "device_hash": "dev_new_abc",
            "browser": "Chrome 148",
            "os": "Windows 10"
        },
        "metadata": {}
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp",
    event_type: "SIGNUP",
    user_id: "usr_new_001",
    ip_address: "203.0.113.50",
    device: { device_hash: "dev_new_abc", browser: "Chrome 148", os: "Windows 10" },
    metadata: {}
  })
})
.then(r => r.json())
.then(console.log)
Response
{
  "risk_score": 35,
  "risk_level": "MEDIUM",
  "action": "MFA_REQUIRED",
  "signals": ["NEW_DEVICE_FOR_USER", "NEW_IP_FOR_USER"]
}
Signals breakdown:
  • NEW_DEVICE_FOR_USER +20 — Device never seen for this user (auto-detected from Redis/SQLite)
  • NEW_IP_FOR_USER +15 — IP never seen for this user (auto-detected)

Total: 35 — Below block threshold (70), above MFA threshold (30). User is prompted for MFA. After this login, the device and IP are tracked in Redis, so subsequent logins from the same device/IP will score 0.

Brute Force from VPN BLOCK

An attacker uses a VPN with 5 failed login attempts, unknown device, and unknown IP. The IP is also flagged in an external threat intel feed.

curl -X POST https://securex.yourdomain.com/api/v1/auth/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "LOGIN",
    "user_id": "usr_abc123",
    "ip_address": "198.51.100.99",
    "device": {
      "device_hash": "dev_attacker_999",
      "browser": "curl/7.68",
      "os": "Linux"
    },
    "metadata": {
      "failed_attempts": 5
    }
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/auth/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "LOGIN",
        "user_id": "usr_abc123",
        "ip_address": "198.51.100.99",
        "device": {"device_hash": "dev_attacker_999", "browser": "curl/7.68", "os": "Linux"},
        "metadata": {"failed_attempts": 5}
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "LOGIN", user_id: "usr_abc123",
    ip_address: "198.51.100.99",
    device: { device_hash: "dev_attacker_999", browser: "curl/7.68", os: "Linux" },
    metadata: { failed_attempts: 5 }
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 105,
  "risk_level": "HIGH",
  "action": "BLOCK",
  "signals": [
    "FAILED_LOGIN_BURST", "THREAT_INTEL_IP", "VPN_DETECTED",
    "NEW_DEVICE_FOR_USER", "NEW_IP_FOR_USER"
  ]
}
Signals breakdown:
  • FAILED_LOGIN_BURST +30 — 5 failed attempts (≥3)
  • THREAT_INTEL_IP +50 — IP found in internal threat intel
  • VPN_DETECTED +40 — IP belongs to a known VPN provider
  • NEW_DEVICE_FOR_USER +20 — New device for this user
  • NEW_IP_FOR_USER +15 — New IP for this user

Total: 155, clamped to 100 → BLOCK. Multiple high-severity signals combine to an automatic block. A webhook is fired if the tenant has webhook_url configured.

Known User, Known Device ALLOW

A returning user logs in from their usual device and IP. The engine has seen this combination before (prev events in SQLite + Redis cache).

curl -X POST https://securex.yourdomain.com/api/v1/auth/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "LOGIN",
    "user_id": "usr_returning_001",
    "ip_address": "49.12.34.56",
    "device": {
      "device_hash": "dev_known_hash123",
      "browser": "Chrome 148",
      "os": "Windows 10"
    },
    "metadata": {}
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/auth/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "LOGIN",
        "user_id": "usr_returning_001",
        "ip_address": "49.12.34.56",
        "device": {
            "device_hash": "dev_known_hash123",
            "browser": "Chrome 148",
            "os": "Windows 10"
        },
        "metadata": {}
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "LOGIN",
    user_id: "usr_returning_001", ip_address: "49.12.34.56",
    device: { device_hash: "dev_known_hash123", browser: "Chrome 148", os: "Windows 10" },
    metadata: {}
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 0,
  "risk_level": "LOW",
  "action": "ALLOW",
  "signals": []
}

Total: 0 — No risk signals triggered. The device and IP are known for this user (auto-detected from Redis), no failed attempts, no VPN, no threat intel hits. Login is allowed immediately without MFA.

New Device, Known IP MFA

A user logs in from a new laptop but the same home IP address. Their usual device+IP combo has history, so the IP is known but the device is not.

curl -X POST https://securex.yourdomain.com/api/v1/auth/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "LOGIN",
    "user_id": "usr_returning_001",
    "ip_address": "49.12.34.56",
    "device": {
      "device_hash": "dev_new_laptop_456",
      "browser": "Firefox 136",
      "os": "macOS 15"
    },
    "metadata": {}
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/auth/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "LOGIN",
        "user_id": "usr_returning_001",
        "ip_address": "49.12.34.56",
        "device": {
            "device_hash": "dev_new_laptop_456",
            "browser": "Firefox 136",
            "os": "macOS 15"
        },
        "metadata": {}
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "LOGIN",
    user_id: "usr_returning_001", ip_address: "49.12.34.56",
    device: { device_hash: "dev_new_laptop_456", browser: "Firefox 136", os: "macOS 15" },
    metadata: {}
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 20,
  "risk_level": "LOW",
  "action": "ALLOW",
  "signals": ["NEW_DEVICE_FOR_USER"]
}
Signals breakdown:
  • NEW_DEVICE_FOR_USER +20 — This device hash hasn't been seen for this user before

Total: 20 — Below MFA threshold (default 30). Login is allowed. If your tenant has a stricter MFA threshold (e.g. 15), this would require MFA. Consider lowering mfa_threshold per tenant if new-device logins should always challenge.

POST Payment Risk Assessment

/api/v1/payment/risk/assess

Analyzes payment transactions for fraud signals — amount velocity, BIN-country mismatch, card enumeration, refund abuse, and more.

Large Electronics Purchase REVIEW

A customer buys a $1,299 laptop with a new payment method. The billing country matches the IP country.

curl -X POST https://securex.yourdomain.com/api/v1/payment/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "PAYMENT",
    "user_id": "usr_abc123",
    "ip_address": "49.12.34.56",
    "device_hash": "dev_known_001",
    "amount": 1299.00,
    "currency": "USD",
    "payment_method": {
      "card_fingerprint": "fp_laptop_xyz",
      "card_bin": "411111",
      "card_scheme": "visa",
      "billing_country": "US"
    },
    "metadata": {
      "new_payment_method": true,
      "product_category": "electronics"
    }
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/payment/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "PAYMENT",
        "user_id": "usr_abc123",
        "ip_address": "49.12.34.56",
        "device_hash": "dev_known_001",
        "amount": 1299.00,
        "currency": "USD",
        "payment_method": {
            "card_fingerprint": "fp_laptop_xyz",
            "card_bin": "411111",
            "card_scheme": "visa",
            "billing_country": "US"
        },
        "metadata": {
            "new_payment_method": True,
            "product_category": "electronics"
        }
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/payment/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "PAYMENT",
    user_id: "usr_abc123", ip_address: "49.12.34.56",
    device_hash: "dev_known_001", amount: 1299.00, currency: "USD",
    payment_method: { card_fingerprint: "fp_laptop_xyz", card_bin: "411111",
      card_scheme: "visa", billing_country: "US" },
    metadata: { new_payment_method: true, product_category: "electronics" }
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 55,
  "risk_level": "MEDIUM",
  "action": "REVIEW",
  "signals": ["LARGE_TXN_AMOUNT", "NEW_PAYMENT_METHOD"]
}
Signals breakdown:
  • LARGE_TXN_AMOUNT +30 — Amount ≥ $1,000
  • NEW_PAYMENT_METHOD +20 — First use of this card fingerprint for this user

Total: 55 — Medium risk, flagged for manual review. The large amount combined with a new payment method is suspicious but not definitive fraud. The BIN matches the US billing country, so no BIN-country mismatch signal was fired.

BIN-Country Mismatch BLOCK

A card issued in Russia (BIN 427432) is used from a Nigerian IP address with US billing address. Classic cross-border fraud pattern.

curl -X POST https://securex.yourdomain.com/api/v1/payment/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "PAYMENT",
    "user_id": "usr_suspicious_002",
    "ip_address": "196.45.33.10",
    "device_hash": "dev_unknown_888",
    "amount": 89.99,
    "currency": "USD",
    "payment_method": {
      "card_fingerprint": "fp_ru_card_001",
      "card_bin": "427432",
      "card_scheme": "mastercard",
      "billing_country": "US"
    },
    "metadata": {}
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/payment/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "PAYMENT",
        "user_id": "usr_suspicious_002",
        "ip_address": "196.45.33.10",
        "device_hash": "dev_unknown_888",
        "amount": 89.99,
        "currency": "USD",
        "payment_method": {
            "card_fingerprint": "fp_ru_card_001",
            "card_bin": "427432",
            "card_scheme": "mastercard",
            "billing_country": "US"
        },
        "metadata": {}
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/payment/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "PAYMENT",
    user_id: "usr_suspicious_002", ip_address: "196.45.33.10",
    device_hash: "dev_unknown_888", amount: 89.99, currency: "USD",
    payment_method: { card_fingerprint: "fp_ru_card_001", card_bin: "427432",
      card_scheme: "mastercard", billing_country: "US" },
    metadata: {}
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 60,
  "risk_level": "HIGH",
  "action": "BLOCK",
  "signals": ["BIN_COUNTRY_MISMATCH", "NEW_DEVICE_FOR_USER"]
}
Signals breakdown:
  • BIN_COUNTRY_MISMATCH +45 — Card BIN (Russia) doesn't match IP country (Nigeria) or billing country (US)
  • NEW_DEVICE_FOR_USER +20 — Device never seen before for this user (auto-detected from auth history)

Total: 65 → clamped to 65 — Below block threshold (70) but above MFA threshold (30). The BIN-country mismatch is a strong fraud indicator. Action is BLOCK because the payment engine uses action: "BLOCK" for HIGH risk (unlike auth which uses BLOCK at 70).

Refund Velocity Abuse BLOCK

A user initiates 3 refunds from the same device within an hour — typical return fraud / wardrobing pattern.

curl -X POST https://securex.yourdomain.com/api/v1/payment/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "REFUND",
    "user_id": "usr_refund_003",
    "ip_address": "203.0.113.50",
    "device_hash": "dev_known_001",
    "amount": 49.99,
    "currency": "USD",
    "payment_method": {
      "card_fingerprint": "fp_return_card",
      "card_bin": "411111"
    },
    "metadata": {
      "refund_velocity_1h": 3
    }
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/payment/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "REFUND",
        "user_id": "usr_refund_003",
        "ip_address": "203.0.113.50",
        "device_hash": "dev_known_001",
        "amount": 49.99,
        "currency": "USD",
        "payment_method": {
            "card_fingerprint": "fp_return_card",
            "card_bin": "411111"
        },
        "metadata": {"refund_velocity_1h": 3}
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/payment/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "REFUND",
    user_id: "usr_refund_003", ip_address: "203.0.113.50",
    device_hash: "dev_known_001", amount: 49.99, currency: "USD",
    payment_method: { card_fingerprint: "fp_return_card", card_bin: "411111" },
    metadata: { refund_velocity_1h: 3 }
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 55,
  "risk_level": "HIGH",
  "action": "BLOCK",
  "signals": ["REFUND_ABUSE"]
}
Signals breakdown:
  • REFUND_ABUSE +40 — ≥3 refunds from same device/IP in last hour

Total: 40 — The refund abuse signal alone is enough to BLOCK. No other signals are needed. This threshold is intentionally lower for refunds, as refund fraud is a high-risk activity.

POST Account Takeover Protection

/api/v1/account/risk/assess

Detects account takeover attempts by analyzing password changes, email changes, recovery requests, and session anomalies.

Password Change 2 Minutes After Login CHALLENGE

A user logs in, then immediately (2 minutes later) requests a password change from a completely different IP and User-Agent. The session ID is the same.

curl -X POST https://securex.yourdomain.com/api/v1/account/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "PASSWORD_CHANGE",
    "user_id": "usr_abc123",
    "ip_address": "198.51.100.50",
    "device_hash": "dev_new_attacker",
    "change_type": "password",
    "session_id": "sess_xyz789",
    "metadata": {
      "minutes_since_login": 2,
      "previous_ip": "49.12.34.56",
      "previous_user_agent": "Mozilla/5.0 Chrome/148",
      "user_agent": "curl/7.68.0",
      "simultaneous_sessions": true
    },
    "old_value": "old_password_hash",
    "new_value": "new_password_hash"
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/account/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "PASSWORD_CHANGE",
        "user_id": "usr_abc123",
        "ip_address": "198.51.100.50",
        "device_hash": "dev_new_attacker",
        "change_type": "password",
        "session_id": "sess_xyz789",
        "metadata": {
            "minutes_since_login": 2,
            "previous_ip": "49.12.34.56",
            "previous_user_agent": "Mozilla/5.0 Chrome/148",
            "user_agent": "curl/7.68.0",
            "simultaneous_sessions": True
        },
        "old_value": "old_password_hash",
        "new_value": "new_password_hash"
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/account/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "PASSWORD_CHANGE",
    user_id: "usr_abc123", ip_address: "198.51.100.50",
    device_hash: "dev_new_attacker",
    change_type: "password", session_id: "sess_xyz789",
    metadata: {
      minutes_since_login: 2, previous_ip: "49.12.34.56",
      previous_user_agent: "Mozilla/5.0 Chrome/148",
      user_agent: "curl/7.68.0", simultaneous_sessions: True
    },
    old_value: "old_password_hash", new_value: "new_password_hash"
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 95,
  "risk_level": "HIGH",
  "action": "CHALLENGE",
  "signals": [
    "SESSION_HIJACK", "PASSWORD_CHANGE_ANOMALY",
    "SIMULTANEOUS_SESSIONS", "NEW_DEVICE_FOR_USER"
  ]
}
Signals breakdown:
  • SESSION_HIJACK +60 — IP AND User-Agent changed within the same session
  • PASSWORD_CHANGE_ANOMALY +35 — Password change within 10 minutes of login
  • SIMULTANEOUS_SESSIONS +45 — User has active sessions from different locations
  • NEW_DEVICE_FOR_USER +20 — Device never seen before

Total: 160, clamped to 100 → CHALLENGE. This is a classic session hijack: the attacker stole the session cookie and immediately changed the password. The CHALLENGE action means the user should be prompted for identity verification (email OTP, security questions) before the change proceeds.

Credential Stuffing Attack BLOCK

An attacker uses a list of stolen credentials, rapidly trying each one from the same IP. 15 attempts in the last minute against 5 different user accounts.

curl -X POST https://securex.yourdomain.com/api/v1/account/risk/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant": "acme_corp",
    "event_type": "PASSWORD_RESET",
    "user_id": "usr_victim_005",
    "ip_address": "45.33.32.10",
    "device_hash": "dev_scanner_pro",
    "metadata": {
      "attempts_per_minute": 15,
      "unique_users_per_ip": 5
    }
  }'
import requests

resp = requests.post(
    "https://securex.yourdomain.com/api/v1/account/risk/assess",
    headers={"X-API-Key": "your-api-key"},
    json={
        "tenant": "acme_corp",
        "event_type": "PASSWORD_RESET",
        "user_id": "usr_victim_005",
        "ip_address": "45.33.32.10",
        "device_hash": "dev_scanner_pro",
        "metadata": {
            "attempts_per_minute": 15,
            "unique_users_per_ip": 5
        }
    }
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/account/risk/assess", {
  method: "POST",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({
    tenant: "acme_corp", event_type: "PASSWORD_RESET",
    user_id: "usr_victim_005", ip_address: "45.33.32.10",
    device_hash: "dev_scanner_pro",
    metadata: { attempts_per_minute: 15, unique_users_per_ip: 5 }
  })
}).then(r => r.json()).then(console.log)
Response
{
  "risk_score": 55,
  "risk_level": "HIGH",
  "action": "BLOCK",
  "signals": ["CREDENTIAL_STUFFING"]
}
Signals breakdown:
  • CREDENTIAL_STUFFING +55 — ≥10 attempts/min OR ≥5 unique users from same IP

Total: 55 — The credential stuffing signal alone exceeds the block threshold for ATO events. The request is blocked entirely. The IP should be added to the threat intel database for future protection.

POST Unified Check

/api/v1/check

A single endpoint that auto-routes to the correct engine based on event_type. Send the same payload as the engine-specific endpoints — the engine handles dispatch.

Event Type Dispatch Table Auto-Route

Instead of calling /auth/risk/assess, /payment/risk/assess, and /account/risk/assess separately, use /v1/check and the engine picks the right path.

event_typeRouted To
LOGINAuth Risk Engine
SIGNUPAuth Risk Engine
PAYMENTPayment Risk Engine
REFUNDPayment Risk Engine
PASSWORD_CHANGEATO Engine
EMAIL_CHANGEATO Engine
RECOVERYATO Engine
PASSWORD_RESETATO Engine

Tip: Use /v1/check for client-side simplicity — only one endpoint to integrate. Use the engine-specific endpoints for finer-grained control.

GET Data Lookup

Read-only endpoints to query device/IP history and list blocked events. All require the same X-API-Key authentication as the risk assessment endpoints.

Look Up Device History Query

Query the engine's historical data for a specific device hash. Returns Redis-cached summary (scores, signals, associated users, last seen) for rapid risk investigation.

curl -X GET https://securex.yourdomain.com/api/v1/auth/risk/device/dev_known_001 \
  -H "X-API-Key: your-api-key"
import requests

resp = requests.get(
    "https://securex.yourdomain.com/api/v1/auth/risk/device/dev_known_001",
    headers={"X-API-Key": "your-api-key"},
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/device/dev_known_001", {
  headers: { "X-API-Key": "your-api-key" }
}).then(r => r.json()).then(console.log)
Response
{
  "entity_id": "dev_known_001",
  "summary": {
    "total_events": 12,
    "last_seen": "2026-05-29T10:30:00Z",
    "associated_users": ["usr_returning_001"],
    "risk_scores": [0, 0, 35, 0, 0],
    "actions": ["ALLOW", "ALLOW", "MFA_REQUIRED", "ALLOW", "ALLOW"],
    "is_blocked": false,
    "threat_intel_hits": 0
  }
}

Tip: Use this endpoint for manual fraud investigation or to verify whether a device was previously flagged. The summary is built from Redis-cached data (last 30 days). Combine with the IP lookup endpoint for a complete picture.

Look Up IP Risk Signals Query

Check an IP address's complete risk profile: associated users, past actions, VPN/proxy status, and threat intel matches.

curl -X GET https://securex.yourdomain.com/api/v1/auth/risk/ip/49.12.34.56 \
  -H "X-API-Key: your-api-key"
import requests

resp = requests.get(
    "https://securex.yourdomain.com/api/v1/auth/risk/ip/49.12.34.56",
    headers={"X-API-Key": "your-api-key"},
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/ip/49.12.34.56", {
  headers: { "X-API-Key": "your-api-key" }
}).then(r => r.json()).then(console.log)
Response
{
  "entity_id": "49.12.34.56",
  "summary": {
    "total_events": 8,
    "last_seen": "2026-05-29T12:00:00Z",
    "associated_users": ["usr_returning_001", "usr_abc123"],
    "risk_scores": [0, 0, 20, 0],
    "actions": ["ALLOW", "ALLOW", "ALLOW", "ALLOW"],
    "country": "DE",
    "is_vpn": false,
    "is_proxy": false,
    "is_tor": false,
    "abuse_score": 0
  }
}

Tip: The summary includes GeoIP country, VPN/proxy detection, and AbuseIPDB score if available. A high abuse score or VPN flag combined with a new device is a strong fraud indicator.

List Blocked Events Audit

Retrieve all events where the engine returned action: "BLOCK". Supports pagination and optional filtering by tenant and event type.

curl -X GET "https://securex.yourdomain.com/api/v1/events/blocked?limit=10&offset=0&event_type=LOGIN" \
  -H "X-API-Key: your-api-key"
import requests

resp = requests.get(
    "https://securex.yourdomain.com/api/v1/events/blocked",
    params={"limit": 10, "offset": 0, "event_type": "LOGIN"},
    headers={"X-API-Key": "your-api-key"},
)
data = resp.json()
print(f"Total blocked: {data['total']}")
for ev in data["events"]:
    print(f"  [{ev['id']}] {ev['user_id']} score={ev['risk_score']} signals={ev['signals']}")
fetch("/api/v1/events/blocked?limit=10&offset=0&event_type=LOGIN", {
  headers: { "X-API-Key": "your-api-key" }
})
.then(r => r.json())
.then(data => {
  console.log("Total blocked:", data.total);
  data.events.forEach(ev => {
    console.log(ev.id, ev.user_id, ev.risk_score, ev.signals);
  });
});
Response
{
  "events": [
    {
      "id": 42,
      "tenant": "acme_corp",
      "user_id": "usr_attacker_001",
      "event_type": "LOGIN",
      "ip_address": "198.51.100.99",
      "device_hash": "dev_malware_xyz",
      "browser": "curl/7.68",
      "os": "Linux",
      "country": "RU",
      "risk_score": 95,
      "risk_level": "HIGH",
      "action": "BLOCK",
      "signals": ["THREAT_INTEL_IP", "FAILED_LOGIN_BURST", "VPN_DETECTED"],
      "created_at": "2026-05-29T10:15:00Z"
    }
  ],
  "total": 1,
  "limit": 10,
  "offset": 0
}
Query parameters:
  • tenant — Filter by tenant (auto-scoped to your API key unless using admin key)
  • event_type — Filter by event type (LOGIN, SIGNUP, PAYMENT, etc.)
  • limit — Max results (default 50, max 500)
  • offset — Pagination offset (default 0)

Tip: Use this endpoint for security audits, SIEM integration, or populating a "blocked activity" dashboard. Events are sorted by created_at DESC (most recent first).

WS WebSocket Dashboard

/admin/ws

Stream real-time risk events to your own dashboard or monitoring system. No authentication required on the WebSocket (admin auth is handled via the admin page login flow).

Real-Time Event Monitoring Stream

Connect to the WebSocket to receive every risk event as it's analyzed. Use this for live dashboards, SIEM integration, or custom alerting.

// Connect with auto-reconnect
const ws = new WebSocket("wss://securex.yourdomain.com/admin/ws");

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log("Risk event:", data);

  if (data.action === "BLOCK") {
    showAlert(`BLOCK: ${data.user_id} (score: ${data.risk_score})`);
  }
};

ws.onclose = () => setTimeout(() => connect(), 5000);
import asyncio
import json
import websockets

async def listen():
    async with websockets.connect(
        "wss://securex.yourdomain.com/admin/ws"
    ) as ws:
        async for message in ws:
            data = json.loads(message)
            print(f"[{data['risk_level']}] {data['action']}: "
                  f"{data['user_id']} score={data['risk_score']}")

asyncio.run(listen())
Message Format
{
  "type": "risk_event",
  "tenant": "acme_corp",
  "event_type": "LOGIN",
  "user_id": "usr_abc123",
  "risk_score": 92,
  "risk_level": "HIGH",
  "action": "BLOCK",
  "signals": ["THREAT_INTEL_IP", "FAILED_LOGIN_BURST"]
}

Tip: The built-in admin dashboard at /admin/ uses this WebSocket to power the Live Dashboard tab (charts + recent events feed). Your custom dashboard can do the same — just connect and parse the risk_event messages.

POST Webhook Notifications

(configured per tenant)

When a risk event results in action: "BLOCK", the engine POSTs a signed webhook to the tenant's configured URL. Configure webhook_url and webhook_secret in the admin portal (Tenants tab).

BLOCK → Trigger Automation Webhook

On every BLOCK action, the engine sends a signed JSON payload to your webhook URL. Verify the HMAC signature before processing.

Payload
{
  "event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "tenant": "acme_corp",
  "event_type": "LOGIN",
  "risk_score": 92,
  "action": "BLOCK",
  "signals": ["THREAT_INTEL_IP", "FAILED_LOGIN_BURST"],
  "user_id": "usr_abc123",
  "timestamp": "2026-05-29T12:00:00Z",
  "signature": "sha256=abc123def456..."
}
Verify HMAC Signature (Python)
import hmac, hashlib, json

def verify_webhook(payload: dict, secret: str) -> bool:
    received_sig = payload.pop("signature", "")
    body = json.dumps(payload, separators=(",", ":"))
    expected = "sha256=" + hmac.new(
        secret.encode(), body.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received_sig)

Delivery guarantees: Up to 3 retries with exponential backoff (1s → 2s → 4s). If the endpoint returns 4xx (non-429), the webhook is rejected permanently. After 5 consecutive failures, the URL enters a 60-second circuit breaker cooldown.

📊 Signal Reference

All risk signals grouped by engine. Signals are additive — the total risk_score is the sum of all triggered signal scores, clamped to 0–100.

SignalEngineScoreDescription
FAILED_ATTEMPTS_DETECTEDRule151–2 failed attempts in window
FAILED_LOGIN_BURSTRule30≥3 failed attempts in window
UNKNOWN_IPRule10IP seen fewer than 3 times historically
UNKNOWN_DEVICERule10Device seen fewer than 3 times
VPN_DETECTEDRule40IP is from a known VPN/proxy provider
PROXY_DETECTEDRule30IP is from a known proxy service
HOSTING_DATACENTERRule35IP belongs to a hosting/datacenter provider
TOR_NETWORKRule60IP is a known Tor exit node
HIGH_ABUSE_SCORERule60AbuseIPDB score ≥ 80
MEDIUM_ABUSE_SCORERule30AbuseIPDB score 50–79
LOW_ABUSE_SCORERule15AbuseIPDB score 25–49
NEW_DEVICEDevice25Client explicitly reports new device globally
NEW_DEVICE_FOR_USERDevice20Device never seen for this user (auto-detected or client hint)
NEW_IPDevice15Client explicitly reports new IP globally
NEW_IP_FOR_USERDevice15IP never seen for this user (auto-detected or client hint)
SUSPICIOUS_DEVICEDevice40Device fingerprint has anomalies
LOW_DEVICE_ENTROPYDevice30Very low entropy in browser fingerprint (likely spoofed)
BLOCKED_DEVICEDevice80Device hash is on the blocklist
HIGH_RISK_COUNTRYGeo25IP country is in the high-risk list
IMPOSSIBLE_TRAVELGeo50Same user from distant locations within minutes
HIGH_VELOCITY_REQUESTSVelocity35More than 10 events from this user in 5 min
RAPID_SUCCESSIVE_LOGINSVelocity25Multiple login events within seconds
BLACKLISTED_IPThreat80IP is in the threat intel database
BLACKLISTED_DEVICEThreat80Device is in the threat intel database
THREAT_INTEL_IPThreat50IP found in internal threat intel
THREAT_INTEL_DEVICEThreat50Device found in internal threat intel
AMOUNT_VELOCITYPayment30≥2 transactions from same user in 30 min
BIN_COUNTRY_MISMATCHPayment45Card BIN issuing country ≠ IP/billing country
CARD_ENUMERATIONPayment55Same card fingerprint used by ≥3 different users
TIME_OF_DAY_ANOMALYPayment20Transaction between 1am–5am local time
HIGH_RISK_CURRENCYPayment25Cryptocurrency or high-risk fiat currency
EMPTY_CARD_BINPayment15No BIN provided with the payment method
NEW_PAYMENT_METHODPayment20First payment with this method for the user
REFUND_ABUSEPayment40≥3 refunds from same device/IP in 1 hour
LARGE_TXN_AMOUNTPayment30Transaction amount ≥ $1,000
GEO_VELOCITYATO50Auth events from distant locations within 30 min
CREDENTIAL_STUFFINGATO55≥10 attempts/min or ≥5 unique users from same IP
SESSION_HIJACKATO60IP and User-Agent changed mid-session
PASSWORD_CHANGE_ANOMALYATO35Password change within 10 min of login
EMAIL_CHANGE_ANOMALYATO40Email address change request
RECOVERY_ABUSEATO30≥3 recovery attempts in 1 hour
SIMULTANEOUS_SESSIONSATO45User has active sessions from different locations
DEVICE_CHANGE_FOR_SENSITIVEATO25Sensitive action (email change) from new device

GET PUT DELETE Tenant Configuration

/api/v1/auth/risk/config/{tenant}

Read, update, or reset per-tenant risk thresholds and settings. All requests require the X-API-Key header. The config is scoped to your API key's tenant (admin key can access any tenant).

Read Current Configuration GET

Retrieve the current risk thresholds and settings for your tenant. Use this to verify what values are in effect before making changes.

curl -X GET https://securex.yourdomain.com/api/v1/auth/risk/config/acme_corp \
  -H "X-API-Key: your-api-key"
import requests

resp = requests.get(
    "https://securex.yourdomain.com/api/v1/auth/risk/config/acme_corp",
    headers={"X-API-Key": "your-api-key"},
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/config/acme_corp", {
  headers: { "X-API-Key": "your-api-key" }
}).then(r => r.json()).then(console.log)
Response
{
  "tenant": "acme_corp",
  "webhook_url": "https://hooks.example.com/fraud-alerts",
  "webhook_secret": "whsec_...",
  "mfa_threshold": 30,
  "block_threshold": 70,
  "rate_limit_per_minute": 120,
  "velocity_window_seconds": 300,
  "failed_login_ttl_seconds": 900,
  "suspicious_device_ttl_seconds": 900,
  "api_key": "ak_...",
  "is_active": true,
  "high_risk_countries": ["RU", "NG", "IR", "KP"]
}
Fields explained:
  • mfa_threshold — Risk score at or above this triggers MFA/REVIEW action (default 30)
  • block_threshold — Risk score at or above this triggers BLOCK action (default 70)
  • rate_limit_per_minute — Max API requests per minute for this tenant
  • velocity_window_seconds — Time window for velocity checks
  • high_risk_countries — List of country codes that trigger HIGH_RISK_COUNTRY signal
  • webhook_url / webhook_secret — Webhook destination for BLOCK events
Update Risk Thresholds PUT

After a spike in fraud attacks, tighten the MFA threshold from 30 to 15 and the block threshold from 70 to 50. Only the fields you include are updated.

curl -X PUT https://securex.yourdomain.com/api/v1/auth/risk/config/acme_corp \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "mfa_threshold": 15,
    "block_threshold": 50
  }'
import requests

resp = requests.put(
    "https://securex.yourdomain.com/api/v1/auth/risk/config/acme_corp",
    headers={"X-API-Key": "your-api-key"},
    json={"mfa_threshold": 15, "block_threshold": 50},
)
print(resp.json())
fetch("https://securex.yourdomain.com/api/v1/auth/risk/config/acme_corp", {
  method: "PUT",
  headers: { "X-API-Key": "your-api-key", "Content-Type": "application/json" },
  body: JSON.stringify({ mfa_threshold: 15, block_threshold: 50 })
}).then(r => r.json()).then(console.log)
Response
{
  "tenant": "acme_corp",
  "mfa_threshold": 15,
  "block_threshold": 50,
  "rate_limit_per_minute": 120,
  ... (all other fields unchanged)
}
Updatable fields:
  • mfa_threshold — Lower = stricter (more MFA prompts)
  • block_threshold — Lower = stricter (more blocks)
  • rate_limit_per_minute — Adjust per tenant tier
  • high_risk_countries — Array of ISO country codes
  • webhook_url / webhook_secret — Webhook configuration

DELETE /api/v1/auth/risk/config/{tenant} — Sends a DELETE request to reset all settings for this tenant back to global defaults (no request body needed).

💡 Best Practices

  • Idempotency: Include X-Idempotency-Key header to prevent duplicate processing. The key should be unique per event (e.g. login_{user_id}_{timestamp}). The engine caches responses for idempotent requests for 1 hour.
  • Client timeouts: Set HTTP timeout to at least 10 seconds. The engine typically responds in 50–300ms, but first-time GeoIP lookups and IP intel enrichment can add latency.
  • IP detection: Pass the end-user's real IP address in ip_address. Do NOT pass the load balancer or server IP. The engine does not trust X-Forwarded-For — it uses the value you provide.
  • Device fingerprinting: Use a deterministic device hash (e.g. WebGL fingerprint + screen resolution + installed fonts) so the same device always produces the same hash. Avoid random UUIDs per session.
  • Metadata hints: If your frontend already knows whether a device/IP is new (e.g. from local storage), pass known_device / known_ip in metadata to skip the engine's internal lookup. If omitted, the engine auto-detects from Redis + SQLite history.
  • Webhook retry: Your webhook endpoint should respond within 5 seconds. The engine retries 3 times (1s, 2s, 4s backoff). Respond with 2xx to acknowledge, 4xx to permanently reject, or 5xx/429 to trigger retry.
  • Redis Sentinel: For production, set REDIS_MODE=sentinel with 3 Sentinel nodes. The engine auto-failovers and degrades gracefully to in-memory fallback if Redis is completely unavailable.
  • Threshold tuning: Adjust mfa_threshold and block_threshold per tenant in the admin portal. Defaults: MFA at 30, BLOCK at 70. Lower MFA threshold for stricter security.