API Reference
All endpoints are under the base path /api/v1.
SaaS base URL: https://demo.humifortis.com/api/v1
Self-hosted: https://your-host:8080/api/v1
Authentication
All requests require an API key in the X-API-Key header.
X-API-Key: hf_your-api-key-here
API keys have scope-based access control:
| Scope | Allowed operations |
|---|---|
events:write | POST /events, POST /evaluate, MFA signal endpoints |
risk:read | GET /risk/:id, GET /decision/:id |
read:audit | Read audit logs |
admin:risks | Reset risk scores, remediation actions |
Rate Limiting
- Enterprise plan: 9 999 req/s per key
- Pro plan: 1 000 req/s per key
- Free tier: 60 req/min (IP-based if unauthenticated)
Rate limit headers are included in every response. On limit: 429 Too Many Requests.
POST /events
Submit one or more security events for async risk scoring.
Scope required: events:write
Request
{
"event": {
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"flow_id": "auth-session-id-or-trace-id",
"entity_id": "user:keycloak:myrealm:e4b96c3a-1234-5678-abcd-ef0123456789",
"entity_type": "user",
"event_type": "auth.login.failed",
"timestamp": "2025-12-16T10:00:00Z",
"source": "keycloak",
"metadata": {
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"device_id": "fp_abc123def456",
"country": "US",
"city": "Portland",
"failed_attempts": 5
}
}
}
For bulk ingestion, use the events array instead:
{
"events": [
{ "event_id": "...", "entity_id": "...", "event_type": "auth.login.failed", "..." },
{ "event_id": "...", "entity_id": "...", "event_type": "auth.login.success", "..." }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
event_id | string | yes | Unique ID from your system (UUID recommended) |
flow_id | string | no | Correlation ID for the auth session or trace |
entity_id | string | yes | Identity of the subject (see Entity IDs) |
entity_type | string | yes | user, session, token, api_key, device |
event_type | string | yes | Dot-namespaced event name (see Event Types) |
timestamp | string | yes | ISO 8601 UTC |
source | string | no | System that generated the event (keycloak, nginx, etc.) |
metadata | object | no | IP, UA, device_id, location, and any custom fields |
Response — 202 Accepted
{
"accepted": 10,
"rejected": 2,
"errors": [
"Event #5: invalid event_type 'unknown.event'",
"Event #7: missing entity_id"
]
}
Events are processed asynchronously. 202 means the event was queued. Risk state in Redis is updated within milliseconds.
GET /risk/:entity_id
Retrieve the current risk state for an entity.
Scope required: risk:read
Request
GET /api/v1/risk/user%3Akeycloak%3Amyrealm%3Ae4b96c3a-1234-5678-abcd-ef0123456789
X-API-Key: hf_...
URL-encode the entity_id if it contains colons.
Response — 200 OK
{
"entity_id": "user:keycloak:myrealm:e4b96c3a-1234-5678-abcd-ef0123456789",
"entity_type": "user",
"risk_score": 71.2,
"risk_level": "HIGH",
"event_count": 15,
"contributing_factors": [
"AUTH_LOGIN_FAILED",
"AUTH_BRUTE_FORCE",
"IMPOSSIBLE_TRAVEL"
],
"last_updated": "2025-12-16T10:00:01Z"
}
| Risk Level | Score Range |
|---|---|
| MINIMAL | 0 – 20 |
| LOW | 20 – 40 |
| MEDIUM | 40 – 60 |
| HIGH | 60 – 80 |
| CRITICAL | 80 – 100 |
POST /evaluate
Evaluate a login attempt synchronously and receive an enforcement decision. This is the primary endpoint called by the Keycloak connector at login time.
Scope required: events:write
Request
{
"event": {
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"flow_id": "auth-session-abc123",
"entity_id": "user:keycloak:myrealm:e4b96c3a-1234-5678-abcd-ef0123456789",
"entity_type": "user",
"event_type": "auth.login.attempt",
"timestamp": "2025-12-16T10:00:00Z",
"source": "keycloak",
"metadata": {
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0...",
"device_id": "fp_abc123def456",
"country": "US",
"city": "Portland"
}
},
"user_mfa_profile": {
"enrolled_methods": ["TOTP", "WEBAUTHN"],
"email_verified": true,
"has_email": true
},
"available_methods": ["TOTP"]
}
| Field | Type | Required | Description |
|---|---|---|---|
event | object | yes | The login event (same shape as POST /events) |
user_mfa_profile | object | no | User's enrolled MFA methods and email status |
user_mfa_profile.enrolled_methods | string[] | no | Available methods: TOTP, WEBAUTHN, EMAIL_OTP |
user_mfa_profile.email_verified | bool | no | Whether the user's email is verified in Keycloak |
user_mfa_profile.has_email | bool | no | Whether the user has an email address |
available_methods | string[] | no | Methods available at this moment (fallback if no profile) |
Response — 200 OK
{
"entity_id": "user:keycloak:myrealm:e4b96c3a-1234-5678-abcd-ef0123456789",
"risk_score": 71.2,
"risk_level": "HIGH",
"event_count": 15,
"action": "CHALLENGE_MFA",
"enforced_action": "CHALLENGE_MFA",
"playbook_rule": "rule_brute_force_step_up",
"contributing_factors": ["AUTH_LOGIN_FAILED", "AUTH_BRUTE_FORCE"],
"derived_signals": ["brute_force_detected", "suspicious_location"],
"device_is_new": false,
"device_is_trusted": true,
"recommended_mfa_method": "TOTP",
"otp_policy": {
"validity_seconds": 120,
"max_attempts": 5
},
"mode": "enforce",
"enforcement_mode": "enforce",
"processed_at": "2025-12-16T10:00:01Z",
"latency_ms": 42,
"confidence": 0.95,
"signal_traces": [
{
"detector": "velocity_analyzer",
"signal": "AUTH_BRUTE_FORCE",
"weight": 0.45,
"value": "5 failures in 60s"
}
]
}
Action values
| Action | Meaning |
|---|---|
ALLOW | Login permitted — proceed without additional challenge |
CHALLENGE_MFA | MFA step-up required before access is granted |
BLOCK | Login rejected — user is not permitted to proceed |
The action is the raw engine decision. enforced_action reflects the actual enforcement after mode (enforce / dry_run / shadow) is applied.
Entity IDs
Entity IDs are free-form strings. The Keycloak connector uses:
user:keycloak:{realm_id}:{user_id}
For custom integrations, you can use any stable identifier:
user:alice@example.com
user:uuid:550e8400-e29b-41d4-a716-446655440000
service-account:my-service
device:fp_abc123
Event Types
Common event types:
| Event type | Description |
|---|---|
auth.login.attempt | Used with /evaluate for inline RBA |
auth.login.success | Successful login |
auth.login.failed | Failed login attempt |
auth.mfa.success | MFA challenge passed |
auth.mfa.failed | MFA challenge failed |
auth.logout | Session terminated |
auth.password.change | Password updated |
auth.account.locked | Account disabled |
waf.attack.blocked | WAF blocked attack |
edr.malware.detected | EDR malware alert |
dlp.exfiltration | Data exfiltration attempt |
Use dot-namespaced names. Unknown event types are scored with neutral weight and logged.
Connector Headers (Keycloak connector)
The Keycloak connector sends these additional headers on every request:
X-Connector-Type: keycloak
X-Connector-Version: 1.0.0
X-Tenant-ID: {tenant_id} (when MULTI_TENANT=true)
You don't need to set these from custom integrations.
Error Responses
{
"error": "invalid_request",
"message": "missing required field: entity_id",
"request_id": "req_abc123"
}
| Status | Meaning |
|---|---|
400 | Malformed JSON or missing required fields |
401 | Invalid or missing API key |
403 | API key valid but insufficient scope |
404 | Entity not found (risk/decision queries) |
429 | Rate limit exceeded |
500 | Internal server error |
gRPC
The Humifortis engine also exposes a gRPC interface on the same port (8080) via cmux. Proto definitions are in the api/proto/ directory of the engine repository.
See Also
- Keycloak Connector — how the connector uses these endpoints
- Quick Start — send your first event in 5 minutes
- Signals — full list of canonical signals and risk weights