All API endpoints require authentication via X-API-Key header or Authorization: Bearer <token>. Base URL: https://securex.yourdomain.com
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",...}'
| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests per minute for this tenant |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When rate limited, the API returns 429 Too Many Requests with a Retry-After header.
| Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid JSON |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | IP not whitelisted, tenant inactive |
| 404 | Not Found | Invalid endpoint |
| 422 | Validation Error | Field type/format errors (see response for details) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Error | Server-side failure (retry with backoff) |
Evaluates authentication events (login, signup) and returns a risk score with an actionable decision.
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)
{
"risk_score": 35,
"risk_level": "MEDIUM",
"action": "MFA_REQUIRED",
"signals": ["NEW_DEVICE_FOR_USER", "NEW_IP_FOR_USER"]
}
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.
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)
{
"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"
]
}
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.
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)
{
"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.
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)
{
"risk_score": 20,
"risk_level": "LOW",
"action": "ALLOW",
"signals": ["NEW_DEVICE_FOR_USER"]
}
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.
Analyzes payment transactions for fraud signals — amount velocity, BIN-country mismatch, card enumeration, refund abuse, and more.
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)
{
"risk_score": 55,
"risk_level": "MEDIUM",
"action": "REVIEW",
"signals": ["LARGE_TXN_AMOUNT", "NEW_PAYMENT_METHOD"]
}
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.
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)
{
"risk_score": 60,
"risk_level": "HIGH",
"action": "BLOCK",
"signals": ["BIN_COUNTRY_MISMATCH", "NEW_DEVICE_FOR_USER"]
}
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).
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)
{
"risk_score": 55,
"risk_level": "HIGH",
"action": "BLOCK",
"signals": ["REFUND_ABUSE"]
}
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.
Detects account takeover attempts by analyzing password changes, email changes, recovery requests, and session anomalies.
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)
{
"risk_score": 95,
"risk_level": "HIGH",
"action": "CHALLENGE",
"signals": [
"SESSION_HIJACK", "PASSWORD_CHANGE_ANOMALY",
"SIMULTANEOUS_SESSIONS", "NEW_DEVICE_FOR_USER"
]
}
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.
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)
{
"risk_score": 55,
"risk_level": "HIGH",
"action": "BLOCK",
"signals": ["CREDENTIAL_STUFFING"]
}
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.
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.
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_type | Routed To |
|---|---|
LOGIN | Auth Risk Engine |
SIGNUP | Auth Risk Engine |
PAYMENT | Payment Risk Engine |
REFUND | Payment Risk Engine |
PASSWORD_CHANGE | ATO Engine |
EMAIL_CHANGE | ATO Engine |
RECOVERY | ATO Engine |
PASSWORD_RESET | ATO Engine |
Tip: Use /v1/check for client-side simplicity — only one endpoint to integrate. Use the engine-specific endpoints for finer-grained control.
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.
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)
{
"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.
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)
{
"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.
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);
});
});
{
"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
}
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).
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).
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())
{
"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.
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).
On every BLOCK action, the engine sends a signed JSON payload to your webhook URL. Verify the HMAC signature before processing.
{
"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..."
}
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.
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.
| Signal | Engine | Score | Description |
|---|---|---|---|
| FAILED_ATTEMPTS_DETECTED | Rule | 15 | 1–2 failed attempts in window |
| FAILED_LOGIN_BURST | Rule | 30 | ≥3 failed attempts in window |
| UNKNOWN_IP | Rule | 10 | IP seen fewer than 3 times historically |
| UNKNOWN_DEVICE | Rule | 10 | Device seen fewer than 3 times |
| VPN_DETECTED | Rule | 40 | IP is from a known VPN/proxy provider |
| PROXY_DETECTED | Rule | 30 | IP is from a known proxy service |
| HOSTING_DATACENTER | Rule | 35 | IP belongs to a hosting/datacenter provider |
| TOR_NETWORK | Rule | 60 | IP is a known Tor exit node |
| HIGH_ABUSE_SCORE | Rule | 60 | AbuseIPDB score ≥ 80 |
| MEDIUM_ABUSE_SCORE | Rule | 30 | AbuseIPDB score 50–79 |
| LOW_ABUSE_SCORE | Rule | 15 | AbuseIPDB score 25–49 |
| NEW_DEVICE | Device | 25 | Client explicitly reports new device globally |
| NEW_DEVICE_FOR_USER | Device | 20 | Device never seen for this user (auto-detected or client hint) |
| NEW_IP | Device | 15 | Client explicitly reports new IP globally |
| NEW_IP_FOR_USER | Device | 15 | IP never seen for this user (auto-detected or client hint) |
| SUSPICIOUS_DEVICE | Device | 40 | Device fingerprint has anomalies |
| LOW_DEVICE_ENTROPY | Device | 30 | Very low entropy in browser fingerprint (likely spoofed) |
| BLOCKED_DEVICE | Device | 80 | Device hash is on the blocklist |
| HIGH_RISK_COUNTRY | Geo | 25 | IP country is in the high-risk list |
| IMPOSSIBLE_TRAVEL | Geo | 50 | Same user from distant locations within minutes |
| HIGH_VELOCITY_REQUESTS | Velocity | 35 | More than 10 events from this user in 5 min |
| RAPID_SUCCESSIVE_LOGINS | Velocity | 25 | Multiple login events within seconds |
| BLACKLISTED_IP | Threat | 80 | IP is in the threat intel database |
| BLACKLISTED_DEVICE | Threat | 80 | Device is in the threat intel database |
| THREAT_INTEL_IP | Threat | 50 | IP found in internal threat intel |
| THREAT_INTEL_DEVICE | Threat | 50 | Device found in internal threat intel |
| AMOUNT_VELOCITY | Payment | 30 | ≥2 transactions from same user in 30 min |
| BIN_COUNTRY_MISMATCH | Payment | 45 | Card BIN issuing country ≠ IP/billing country |
| CARD_ENUMERATION | Payment | 55 | Same card fingerprint used by ≥3 different users |
| TIME_OF_DAY_ANOMALY | Payment | 20 | Transaction between 1am–5am local time |
| HIGH_RISK_CURRENCY | Payment | 25 | Cryptocurrency or high-risk fiat currency |
| EMPTY_CARD_BIN | Payment | 15 | No BIN provided with the payment method |
| NEW_PAYMENT_METHOD | Payment | 20 | First payment with this method for the user |
| REFUND_ABUSE | Payment | 40 | ≥3 refunds from same device/IP in 1 hour |
| LARGE_TXN_AMOUNT | Payment | 30 | Transaction amount ≥ $1,000 |
| GEO_VELOCITY | ATO | 50 | Auth events from distant locations within 30 min |
| CREDENTIAL_STUFFING | ATO | 55 | ≥10 attempts/min or ≥5 unique users from same IP |
| SESSION_HIJACK | ATO | 60 | IP and User-Agent changed mid-session |
| PASSWORD_CHANGE_ANOMALY | ATO | 35 | Password change within 10 min of login |
| EMAIL_CHANGE_ANOMALY | ATO | 40 | Email address change request |
| RECOVERY_ABUSE | ATO | 30 | ≥3 recovery attempts in 1 hour |
| SIMULTANEOUS_SESSIONS | ATO | 45 | User has active sessions from different locations |
| DEVICE_CHANGE_FOR_SENSITIVE | ATO | 25 | Sensitive action (email change) from new device |
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).
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)
{
"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"]
}
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 tenantvelocity_window_seconds — Time window for velocity checkshigh_risk_countries — List of country codes that trigger HIGH_RISK_COUNTRY signalwebhook_url / webhook_secret — Webhook destination for BLOCK eventsAfter 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)
{
"tenant": "acme_corp",
"mfa_threshold": 15,
"block_threshold": 50,
"rate_limit_per_minute": 120,
... (all other fields unchanged)
}
mfa_threshold — Lower = stricter (more MFA prompts)block_threshold — Lower = stricter (more blocks)rate_limit_per_minute — Adjust per tenant tierhigh_risk_countries — Array of ISO country codeswebhook_url / webhook_secret — Webhook configurationDELETE /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).
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.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.known_device / known_ip in metadata to skip the engine's internal lookup. If omitted, the engine auto-detects from Redis + SQLite history.REDIS_MODE=sentinel with 3 Sentinel nodes. The engine auto-failovers and degrades gracefully to in-memory fallback if Redis is completely unavailable.mfa_threshold and block_threshold per tenant in the admin portal. Defaults: MFA at 30, BLOCK at 70. Lower MFA threshold for stricter security.