External Feed API
The External Feed API is a read-only REST API that allows MSSP partners, customer SOC teams, SOAR platforms, and external SIEMs to pull security data from RhythmX. API keys authenticate requests and control access scope — master keys see all entities, scoped keys see only their entity.
How It Works
flowchart LR
A["MSSP Partner<br>or Customer SOC"] -->|"X-API-Key header"| B["API Key<br>Authentication"]
B --> C{"Master Key?"}
C -->|"Yes (entity_name=*)"| D["All Entities<br>or filter by ?entity_name="]
C -->|"No (entity_name=CustomerA)"| E["CustomerA Only"]
D --> F["JSON Response<br><i>Paginated</i>"]
E --> F
style A fill:#4a148c,stroke:#9c27b0,color:#fff
style B fill:#b71c1c,stroke:#f05545,color:#fff
style C fill:#e65100,stroke:#ff9800,color:#fff
style D fill:#0d47a1,stroke:#42a5f5,color:#fff
style E fill:#0d47a1,stroke:#42a5f5,color:#fff
style F fill:#1b5e20,stroke:#4c8c4a,color:#fff
Base URL
https://<your-rhythmx-host>/api/v1/feed
All endpoints are prefixed with /api/v1/feed.
Authentication
Include your API key in the X-API-Key header with every request.
curl -H "X-API-Key: smk_your_api_key_here" \
https://rhythmx.example.com/api/v1/feed/health
Key Types
| Key Type | entity_name in DB |
Behavior |
|---|---|---|
| Master Key | * |
Sees all entities. Pass ?entity_name=X to filter, or omit for all. |
| Scoped Key | CustomerA |
Only sees CustomerA data. Cannot access other entities. |
| Legacy unscoped | (blank) | Treated as a master key. Creating one is no longer permitted — see below. |
The key's entity is the access boundary — not ?entity_name=
A key's entity_name is what actually restricts access. The ?entity_name=
query parameter is supplied by the caller, so it is a convenience filter for
master keys, never an access control: any client holding a master key can change
it or leave it out and receive all entities.
To restrict a consumer to one tenant, issue a scoped key. A scoped key
ignores ?entity_name= entirely, so it cannot be re-targeted at another entity,
and requests for another entity's incident, case or alert by ID return 404.
Blank entity keys
entity_name was previously optional, and a blank value was documented as
granting access to all entities. It did — but such a key also ignored
?entity_name=, so unlike a real master key it could not be narrowed to a
single entity for a given request.
Blank keys now behave exactly like *, so existing integrations are unaffected
and the filter works as expected. New keys must state their scope explicitly —
either an entity name or *. See API Key Management.
Key Properties
- Non-retrievable — plaintext key shown only once at creation
- Revocable — can be disabled or deleted anytime
- Re-scopable — a key's
entity_namecan be changed without reissuing the secret - Expirable — optional expiration date
- Rate-limited — 120 requests per minute per API key (configurable via
FEED_RATE_LIMITenv var)
Scopes and per-key rate limits are informational
The scopes and rate_limit_per_minute fields are stored on the key and shown
in the admin UI, but are not currently enforced. Every valid key can read all
endpoints listed here, and the rate limit applied is the server-wide
FEED_RATE_LIMIT value (counted per key). Plan access around the key's
entity scope, which is enforced, and treat scopes as a label recording
intent rather than a restriction.
Rate Limiting
All external feed endpoints are rate-limited per API key to prevent abuse:
| Setting | Default | Configurable |
|---|---|---|
| Requests per minute | 120 | FEED_RATE_LIMIT in .env.production |
| Scope | Per API key | Each key is counted separately |
| Response when exceeded | 429 Too Many Requests |
Includes Retry-After header |
Example error when rate limit is exceeded:
{
"error": "Rate limit exceeded",
"retry_after": 30
}
Every key is counted separately, but they all share the same limit: the server-wide
FEED_RATE_LIMIT. The rate_limit_per_minute value stored on an individual key is
not currently applied.
Space out bulk sweeps — polling many endpoints back-to-back can trip the limit.
Common Parameters
These parameters are available on all list endpoints:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | Filter by entity (master keys only — scoped keys ignore this) |
since |
string | ISO 8601 timestamp — only return records created/updated after this time |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Incremental Polling (since parameter)
The since parameter enables efficient incremental polling — consumers only pull data that has changed since their last request. Supported on all endpoints that return time-based data.
Accepted formats:
?since=2026-03-20T10:30:00Z (ISO 8601 with timezone)
?since=2026-03-20T10:30:00 (ISO 8601 without timezone — treated as UTC)
?since=2026-03-20 (date only — from midnight UTC)
Example — poll every 5 minutes for new alerts:
# First call — get everything from the last hour
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/alerts?since=2026-03-20T09:30:00Z"
# Subsequent calls — only get what's new since last poll
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/alerts?since=2026-03-20T09:35:00Z"
Endpoints supporting since:
| Endpoint | Time column filtered |
|---|---|
/incidents |
updated_at |
/cases |
updated_at |
/alerts |
system_time |
/alerts/stats |
system_time |
/risks/top-users |
system_time |
/risks/top-computers |
system_time |
/risks/top-rules |
system_time |
/mitre/tactics |
system_time |
/mitre/techniques |
system_time |
Response Format
All endpoints return this structure:
{
"success": true,
"data": [ ... ],
"pagination": {
"total": 150,
"limit": 50,
"offset": 0,
"has_more": true
},
"timestamp": "2026-03-20T10:30:00Z"
}
Endpoints
Health Check
GET /api/v1/feed/health
Returns the API service status.
Example:
curl -H "X-API-Key: smk_abc12345..." \
https://rhythmx.example.com/api/v1/feed/health
Response:
{
"status": "ok",
"service": "external_feed_api"
}
List Incidents
GET /api/v1/feed/incidents
Returns security incidents. Each incident represents a group of related alarms for an actor (user or computer).
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
status |
string | OPEN, IN_PROGRESS, CLOSED, FALSE_POSITIVE |
since |
string | ISO 8601 — incidents updated after this time |
limit |
integer | Results per page (default: 50) |
offset |
integer | Pagination offset |
Example:
# All entities
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/incidents?status=OPEN&limit=20"
# Specific entity
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/incidents?entity_name=Primary%20Site&status=OPEN"
Response:
{
"success": true,
"data": [
{
"incident_id": "INC-a1b2c3d4",
"group_key": "jsmith",
"actor_type": "user",
"entity_name": "Primary Site",
"status": "OPEN",
"assigned_analyst": null,
"first_alarm_time": "2026-03-02T08:15:00Z",
"last_alarm_time": "2026-03-02T14:20:00Z",
"created_at": "2026-03-02T14:22:00Z",
"updated_at": "2026-03-02T14:22:00Z",
"closed_at": null,
"external_ticket_id": null,
"external_system": null,
"severity": "HIGH",
"attack_type": "CREDENTIAL_THEFT,SYSTEM_BINARY_ANOMALY",
"system_state": "ESCALATED"
}
],
"pagination": {
"total": 12,
"limit": 20,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
severity vs system_state — they are computed differently
severity comes from the actor's risk score alone (>=80 CRITICAL,
>=60 HIGH, >=40 MEDIUM, else LOW). system_state also escalates on
correlated threat cases: risk >= 70 OR (cases > 1 AND
risk >= 40).
So an incident can legitimately read MEDIUM while sitting at ESCALATED —
a moderate-risk actor showing several distinct attack patterns. The reverse
also occurs: risk 60-69 with one case is HIGH but only ACTIVE. The two
scales are not aligned at any threshold, so do not derive one from the other
or treat a mismatch as an error.
Get Incident Detail
GET /api/v1/feed/incidents/{incident_id}
Returns full details for a single incident including all fields.
Example:
curl -H "X-API-Key: smk_abc12345..." \
https://rhythmx.example.com/api/v1/feed/incidents/INC-a1b2c3d4
Get Incident + Related Data
GET /api/v1/feed/incidents/{incident_id}/related
Returns the incident and everything correlated to its actor in one call — LogRhythm alarms, RhythmX Sigma alerts, and threat cases — each with a per-rule rollup and a capped record list. All sections are scoped to the incident's entity.
Parameters:
| Parameter | Type | Description |
|---|---|---|
window |
string | incident (default) bounds results to the incident's activity window; all returns the actor's full retained history in this entity. |
limit |
integer | Max records per section (default: 100, max: 500). Rollups and total counts are always complete. |
Example:
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/incidents/INC-a1b2c3d4/related?window=all&limit=250"
Response (abridged):
{
"success": true,
"incident": { "incident_id": "INC-a1b2c3d4", "group_key": "jsmith", "actor_type": "user", "entity_name": "Primary Site" },
"window": { "mode": "incident", "start": "2026-03-02T08:15:00Z", "end": "2026-03-02T14:20:00Z" },
"logrhythm_alarms": { "total": 3, "rule_groups": [ ... ], "alarms": [ ... ], "returned": 3 },
"sigma_alerts": { "total": 49, "unique_rules": 8, "max_risk": 34, "rule_groups": [ ... ], "alerts": [ ... ], "returned": 49 },
"threat_cases": { "total": 2, "cases": [ /* each with alerts_retained / alerts_available / alerts_expired */ ], "returned": 2 },
"limit_per_section": 100,
"timestamp": "2026-03-20T10:30:00Z"
}
See the dedicated Incident Feed API page for the full field-by-field reference of every section.
List Cases
GET /api/v1/feed/cases
Returns threat cases — correlated attack patterns detected by the analytics engine.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
status |
string | OPEN, IN_PROGRESS, CLOSED, FALSE_POSITIVE |
severity |
string | CRITICAL, HIGH, MEDIUM, LOW |
case_type |
string | e.g., LATERAL_MOVEMENT, RANSOMWARE_INDICATORS, BRUTE_FORCE, CREDENTIAL_THEFT, PERSISTENCE, PRIVILEGE_ESCALATION, RECONNAISSANCE |
since |
string | ISO 8601 — cases updated after this time |
limit |
integer | Results per page (default: 50) |
offset |
integer | Pagination offset |
Example:
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/cases?severity=CRITICAL&case_type=RANSOMWARE_INDICATORS"
Response:
{
"success": true,
"data": [
{
"case_id": "CASE-x1y2z3",
"entity_name": "Primary Site",
"case_type": "RANSOMWARE_INDICATORS",
"status": "OPEN",
"severity": "CRITICAL",
"primary_user": "admin",
"affected_computers": "DC01,WS-PC42",
"alert_count": 47,
"max_risk_score": 95,
"first_alert_time": "2026-03-19T03:10:00Z",
"last_alert_time": "2026-03-19T04:45:00Z",
"created_at": "2026-03-19T04:46:00Z",
"updated_at": "2026-03-19T04:46:00Z",
"alerts_retained": 47,
"alerts_available": true,
"alerts_expired": false
}
],
"alert_retention_days": 30,
"pagination": {
"total": 3,
"limit": 50,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
A case outlives the alerts behind it — check alerts_expired
RhythmX keeps Sigma alerts for 30 days (alert_retention_days), but a
threat case is kept indefinitely. An older case therefore still reports the
alert_count it was raised with, while the alerts themselves no longer
exist.
| Field | Meaning |
|---|---|
alerts_retained |
How many of the case's alerts still exist |
alerts_available |
alerts_retained > 0 |
alerts_expired |
alerts_retained == 0 — the evidence is gone |
alerts_retained is the real count, not an inference from dates, so a
partially purged case reports honestly — e.g. alert_count: 47 with
alerts_retained: 12.
Do not treat an empty alert list as an empty case. Check
alerts_expired first: false means the case genuinely had no matching
alerts, true means they aged out. The same three fields appear wherever a
case is returned — /cases/{id},
/incidents/{id}/related and
/risks/actors/{actor}/activity.
This mirrors logs_available / logs_expired on the LogRhythm alarm
drill-down, which exists for the same reason.
Get Case Detail
GET /api/v1/feed/cases/{case_id}
Returns full details for a single threat case, including the evidence-retention fields described under List Cases.
Example:
curl -H "X-API-Key: smk_abc12345..." \
https://rhythmx.example.com/api/v1/feed/cases/CASE-x1y2z3
When the case's alerts have aged out, the response says so explicitly rather than returning an empty alert list:
{
"success": true,
"data": {
"case_id": "CASE-x1y2z3",
"alert_count": 47,
"alerts_retained": 0,
"alerts_available": false,
"alerts_expired": true
},
"alert_retention_days": 30,
"message": "The alerts behind this case are no longer retained. RhythmX keeps Sigma alerts for 30 days; the case itself, its counts and its timeline are retained."
}
message is present only when the evidence has expired, so its presence is
itself a reliable signal.
List Alerts
GET /api/v1/feed/alerts
Returns RhythmX detection alerts — individual rule matches from the detection engine.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
rule_level |
string | informational, low, medium, high, critical |
since |
string | ISO 8601 — alerts with system_time after this |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset |
Example:
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/alerts?rule_level=critical&limit=10"
Response:
{
"success": true,
"data": [
{
"id": 154832,
"title": "Mimikatz Command Line Usage",
"description": "Detects Mimikatz command line arguments",
"rule_level": "critical",
"tags": "attack.credential_access,attack.t1003",
"tactics": "credential_access",
"techniques": "T1003",
"computer_name": "DC01",
"user_id": "admin",
"ip_address": "10.0.5.42",
"entity_name": "Primary Site",
"system_time": "2026-03-19T14:22:00Z",
"risk": 95,
"image_path": "C:\\Windows\\Temp\\mimi.exe",
"command_line": "sekurlsa::logonpasswords",
"count": 1,
"first_seen": "2026-03-19T14:22:00Z",
"last_seen": "2026-03-19T14:22:00Z"
}
],
"pagination": {
"total": 5,
"limit": 10,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
Get Alert Detail
GET /api/v1/feed/alerts/{alert_id}
Returns full details for a single alert including raw log data.
Alert Statistics
GET /api/v1/feed/alerts/stats
Returns aggregate alert statistics.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Example:
curl -H "X-API-Key: smk_abc12345..." \
https://rhythmx.example.com/api/v1/feed/alerts/stats
Response:
{
"success": true,
"data": {
"total_alerts": 45832,
"by_level": {
"critical": 127,
"high": 1893,
"medium": 12450,
"low": 28100,
"informational": 3262
},
"by_entity": [
{ "entity_name": "Primary Site", "count": 28000 },
{ "entity_name": "Branch Office", "count": 17832 }
],
"top_rules": [
{ "title": "Remote Access Tool Execution", "rule_level": "high", "count": 4521 },
{ "title": "Suspicious PowerShell Command", "rule_level": "medium", "count": 3102 }
]
},
"timestamp": "2026-03-20T10:30:00Z"
}
SOC Metrics Overview
GET /api/v1/feed/metrics/overview
Returns high-level SOC metrics — counts across incidents, cases and alerts, plus the risk roll-up. Everything needed for a dashboard's top KPI row in one call.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Example:
curl -H "X-API-Key: smk_abc12345..." \
https://rhythmx.example.com/api/v1/feed/metrics/overview
Response:
{
"success": true,
"data": {
"total_incidents": 342,
"open_incidents": 28,
"total_cases": 89,
"open_cases": 15,
"total_alerts": 45832,
"critical_alerts": 127,
"high_risk_entities": 4,
"entity_risk_score": 88,
"entity_risk_level": "critical",
"risky_users": 32,
"risky_computers": 6,
"critical_actors": 15,
"high_actors": 23
},
"timestamp": "2026-03-20T10:30:00Z"
}
| Field | Meaning |
|---|---|
entity_risk_score / _level |
Highest actor risk in scope. With a scoped key (or ?entity_name=) this is that entity's risk; across all entities it is the worst score anywhere. |
risky_users / risky_computers |
Actors at critical or high risk level — not every actor with an alert. |
high_risk_entities |
Count of entities having critical or high alerts. Distinct from the risk score above. |
Incidents by Severity & Status
GET /api/v1/feed/metrics/incidents/severity
Incidents broken down by severity, by status, and by the two crossed. Every severity and status is returned including zero counts, so a stacked bar or matrix can be drawn without filling in gaps.
Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | (Optional) Only count this status. Omit for all. |
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Response:
{
"success": true,
"data": {
"total_incidents": 210,
"by_severity": [
{ "severity": "CRITICAL", "count": 22 },
{ "severity": "HIGH", "count": 35 },
{ "severity": "MEDIUM", "count": 18 },
{ "severity": "LOW", "count": 135 }
],
"by_status": [
{ "status": "OPEN", "count": 45 },
{ "status": "IN_PROGRESS", "count": 4 },
{ "status": "CLOSED", "count": 161 }
],
"by_severity_and_status": [
{ "severity": "CRITICAL",
"statuses": [ { "status": "OPEN", "count": 20 }, { "status": "IN_PROGRESS", "count": 2 }, { "status": "CLOSED", "count": 0 } ],
"total": 22 }
]
}
}
How incident severity is derived
Severity is the actor's risk score, bucketed — >= 80 CRITICAL,
>= 60 HIGH, >= 40 MEDIUM, otherwise LOW — so it lines up with the risk
levels reported by /risks/actors and
/entities/risk.
It is a point-in-time snapshot, stamped when the incident last synced.
An actor whose risk has moved since will not be reflected until the next
sync, so a bare severity can lag the actor's live score — use
/risks/actors when you need the current value.
Severity is also returned per incident on /incidents as
severity, so a dashboard table needs only one call.
Severity Breakdown
GET /api/v1/feed/metrics/severity
Returns incident and case counts broken down by status and severity.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Response:
{
"success": true,
"data": {
"incidents_by_status": {
"OPEN": 28,
"IN_PROGRESS": 12,
"CLOSED": 285,
"FALSE_POSITIVE": 17
},
"cases_by_severity": {
"CRITICAL": 3,
"HIGH": 22,
"MEDIUM": 41,
"LOW": 23
},
"cases_by_type": {
"LATERAL_MOVEMENT": 12,
"BRUTE_FORCE": 18,
"CREDENTIAL_THEFT": 8,
"PERSISTENCE": 15,
"RANSOMWARE_INDICATORS": 3
}
},
"timestamp": "2026-03-20T10:30:00Z"
}
Mean Time to Respond (MTTR)
GET /api/v1/feed/metrics/mttr
Returns average time from incident creation to closure.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Response:
{
"success": true,
"data": {
"overall_mttr_minutes": 142,
"closed_incidents": 285,
"by_status": {
"CLOSED": { "avg_minutes": 125, "count": 268 },
"FALSE_POSITIVE": { "avg_minutes": 45, "count": 17 }
}
},
"timestamp": "2026-03-20T10:30:00Z"
}
Trends
GET /api/v1/feed/metrics/trends
Returns daily incident and case creation counts over time.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
days |
integer | Number of days to look back (default: 30, max: 90) |
Example:
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/metrics/trends?days=7"
Response:
{
"success": true,
"data": {
"daily": [
{ "date": "2026-03-19", "incidents": 5, "cases": 2 },
{ "date": "2026-03-18", "incidents": 8, "cases": 3 },
{ "date": "2026-03-17", "incidents": 3, "cases": 1 },
{ "date": "2026-03-16", "incidents": 12, "cases": 4 },
{ "date": "2026-03-15", "incidents": 6, "cases": 2 },
{ "date": "2026-03-14", "incidents": 4, "cases": 1 },
{ "date": "2026-03-13", "incidents": 7, "cases": 3 }
]
},
"timestamp": "2026-03-20T10:30:00Z"
}
Incident Ageing
GET /api/v1/feed/metrics/incidents/aging
How long open incidents have been open — the backlog view. Every bucket is always returned, including empty ones, so a chart can be drawn without having to invent the missing categories.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Response:
{
"success": true,
"data": {
"buckets": [
{ "age_bucket": "0-24h", "count": 4, "oldest": "2026-03-19T10:17:34Z", "avg_age_hours": 9.2 },
{ "age_bucket": "1-3d", "count": 7, "oldest": "2026-03-17T08:02:11Z", "avg_age_hours": 51.0 },
{ "age_bucket": "3-7d", "count": 12, "oldest": "2026-03-14T11:40:02Z", "avg_age_hours": 118.4 },
{ "age_bucket": "7-30d", "count": 11, "oldest": "2026-02-20T12:08:31Z", "avg_age_hours": 533.3 },
{ "age_bucket": "30d+", "count": 3, "oldest": "2025-12-27T10:53:24Z", "avg_age_hours": 2586.7 }
],
"open_total": 37,
"older_than_7d": 14,
"oldest_open": "2025-12-27T10:53:24Z",
"avg_age_hours": 421.8
}
}
Age is measured from created_at to now, and only incidents that are not
CLOSED are counted — an ageing report is about what is still outstanding.
/metrics/mttr answers the opposite question: how
long closed incidents took to resolve.
Incidents by Attack Type / MITRE
GET /api/v1/feed/metrics/incidents/attack-types
Incidents grouped by attack category, with the MITRE tactic each maps to, plus a by-tactic roll-up.
Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Incident status to count. Default: open only. ALL for every status. |
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
Response:
{
"success": true,
"data": {
"total_incidents": 45,
"by_attack_type": [
{ "attack_type": "CREDENTIAL_THEFT", "incident_count": 31, "mitre_tactic": "credential-access", "mitre_tactic_id": "TA0006" },
{ "attack_type": "SYSTEM_BINARY_ANOMALY", "incident_count": 17, "mitre_tactic": "defense-evasion", "mitre_tactic_id": "TA0005" },
{ "attack_type": "APT_CHAIN", "incident_count": 3, "mitre_tactic": "multi-stage", "mitre_tactic_id": null }
],
"by_mitre_tactic": [
{ "tactic": "credential-access", "tactic_id": "TA0006", "incident_count": 34, "attack_types": ["CREDENTIAL_THEFT", "PASSWORD_SPRAYING"] },
{ "tactic": "defense-evasion", "tactic_id": "TA0005", "incident_count": 17, "attack_types": ["SYSTEM_BINARY_ANOMALY"] }
],
"unmapped": ["APT_CHAIN"]
}
}
Counts sum above total_incidents — by design
An incident can carry several attack categories, so it is counted once per
distinct category. total_incidents is the unduplicated figure. Categories
with no MITRE equivalent (multi-stage patterns such as APT_CHAIN) are
listed under unmapped.
This is incident-level attribution, derived from the attack categories the correlation engine assigns to each incident. For tactic counts at the individual alert level, use MITRE ATT&CK Tactics below.
MITRE ATT&CK Tactics
GET /api/v1/feed/mitre/tactics
Returns alert counts grouped by MITRE ATT&CK tactic.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{ "tactic": "credential_access", "count": 4521 },
{ "tactic": "lateral_movement", "count": 3102 },
{ "tactic": "execution", "count": 2890 },
{ "tactic": "persistence", "count": 2145 },
{ "tactic": "defense_evasion", "count": 1876 }
],
"pagination": {
"total": 11,
"limit": 50,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
MITRE ATT&CK Techniques
GET /api/v1/feed/mitre/techniques
Returns alert counts grouped by MITRE ATT&CK technique.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{ "technique": "T1003", "count": 2341 },
{ "technique": "T1059", "count": 1892 },
{ "technique": "T1021", "count": 1654 },
{ "technique": "T1053", "count": 1230 },
{ "technique": "T1078", "count": 987 }
],
"pagination": {
"total": 45,
"limit": 50,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
List Entities
GET /api/v1/feed/entities
Returns all entities (sites/customers) with their incident, case, and alert counts.
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{
"entity_name": "Primary Site",
"incident_count": 215,
"case_count": 52,
"alert_count": 28000
},
{
"entity_name": "Branch Office",
"incident_count": 127,
"case_count": 37,
"alert_count": 17832
}
],
"pagination": {
"total": 2,
"limit": 50,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
Entity Risk
GET /api/v1/feed/entities/risk
Per-entity risk roll-up — the overall risk score plus the counts behind it. This is the endpoint for a dashboard's top-line KPI row.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{
"entity_name": "Primary Site",
"risk_score": 88,
"risk_level": "critical",
"avg_actor_risk": 24.5,
"incidents": 210,
"open_incidents": 45,
"risky_users": 32,
"risky_computers": 6,
"critical_actors": 15,
"high_actors": 23,
"total_actors": 1450,
"last_activity": "2026-03-20T06:24:36Z"
}
]
}
Entity risk is the MAX actor risk, not the average
One actor at 95 is precisely what the score exists to surface; averaging it
against thousands of quiet actors would bury it. avg_actor_risk is
returned alongside so you can show both. risky_users and
risky_computers count actors at critical or high risk level — not
every actor that happens to have an alert.
Entity Summary
GET /api/v1/feed/entities/{entity_name}/summary
Returns a detailed summary for a specific entity.
Example:
curl -H "X-API-Key: smk_abc12345..." \
"https://rhythmx.example.com/api/v1/feed/entities/Primary%20Site/summary"
Response:
{
"success": true,
"data": {
"entity_name": "Primary Site",
"incidents": {
"total": 215,
"open": 18,
"in_progress": 8,
"closed": 180,
"false_positive": 9
},
"cases": {
"total": 52,
"critical": 2,
"high": 12,
"medium": 25,
"low": 13
},
"alerts": {
"total": 28000,
"critical": 82,
"high": 1200
},
"top_rules": [
{ "title": "Remote Access Tool Execution", "count": 2800 },
{ "title": "Suspicious PowerShell Command", "count": 1950 }
],
"top_users": [
{ "user_id": "admin", "alert_count": 450, "total_risk": 12500 },
{ "user_id": "svc_backup", "alert_count": 320, "total_risk": 8900 }
],
"top_computers": [
{ "computer_name": "DC01", "alert_count": 890, "total_risk": 24000 },
{ "computer_name": "WS-PC42", "alert_count": 650, "total_risk": 18000 }
]
},
"timestamp": "2026-03-20T10:30:00Z"
}
Top Risky Actors
GET /api/v1/feed/risks/actors
Returns the highest-risk users, computers and IPs ranked by the risk score the platform itself computes — a 0–100 score from seven weighted factors, the same number used internally. This is the endpoint to build a "Top Risky Users" or "Top Risky Computers" panel on.
Parameters:
| Parameter | Type | Description |
|---|---|---|
actor_type |
string | user, host or ip. Omit for all. |
risk_level |
string | critical, high, medium, low |
min_risk |
number | Only actors scoring at or above this (0–100) |
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{
"actor": "jsmith",
"actor_type": "user",
"entity_name": "Primary Site",
"risk_score": 88,
"risk_level": "critical",
"activity": {
"alarms_total": 12, "alarms_7d": 4, "alarms_24h": 1,
"critical_alarms": 2, "high_alarms": 5, "max_rbp": 75,
"sigma_alerts": 240, "threat_cases": 18,
"unique_rules": 8, "unique_tactics": 6, "unique_techniques": 11
},
"risk_factors": {
"threat_cases": 95.0, "sigma_alerts": 88.0, "volume": 76.5,
"velocity": 82.0, "diversity": 71.4, "mitre": 60.0, "recency": 90.0
},
"first_seen": "2026-03-01T14:37:20Z",
"last_seen": "2026-03-20T10:34:13Z"
}
],
"pagination": { "total": 1450, "limit": 50, "offset": 0, "has_more": true }
}
Show the factors, not just the score
risk_factors carries the seven weighted components behind risk_score.
Surfacing them — on hover, or in a drill-down — lets an analyst see why an
actor scores what it does. A bare number invites "why 92?" and is hard to
defend in a review.
Actor Activity (drill-down)
GET /api/v1/feed/risks/actors/{actor}/activity
Everything observed for one actor: risk, LogRhythm alarms, RhythmX Sigma alerts, threat cases and incidents. This is the drill-down behind a top-risky-actor row.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | Required for master keys. Scoped keys supply it automatically. |
actor_type |
string | user, host or ip. Inferred if omitted. |
days |
integer | Look-back window (default: 30, max: 365) |
limit |
integer | Max records per section (default: 100, max: 500) |
An actor is only unique within an entity
The same account name — administrator, svc_backup — exists in many
entities, and they are different accounts. A master key must pass
entity_name; without it the request returns 400 rather than a response
that merges tenants.
Response:
{
"success": true,
"actor": {
"actor": "jsmith", "actor_type": "user", "entity_name": "Primary Site",
"risk_score": 88, "risk_level": "critical",
"first_seen": "2026-03-01T14:37:20Z", "last_seen": "2026-03-20T10:34:13Z"
},
"window": { "days": 30 },
"logrhythm_alarms": { "total": 12, "rule_groups": [ /* per-rule counts */ ] },
"sigma_alerts": { "total": 240, "unique_rules": 8, "max_risk": 95,
"rule_groups": [ /* per-rule counts */ ] },
"threat_cases": { "total": 18, "cases": [ /* … */ ] },
"incidents": { "total": 2, "incidents": [ /* … */ ] }
}
Top Risky Users (legacy)
GET /api/v1/feed/risks/top-users
Returns users ranked by cumulative alert risk — the sum of risk across
their Sigma alerts.
Prefer /risks/actors
This ranks by summed alert risk, which is unbounded: an actor with many
low-risk alerts can outrank one with a genuinely high score. It is kept
unchanged for existing consumers. For a risk ranking that matches the
platform's own, use /risks/actors.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{ "user_id": "admin", "entity_name": "Primary Site", "total_risk": 12500, "alert_count": 450 },
{ "user_id": "svc_backup", "entity_name": "Primary Site", "total_risk": 8900, "alert_count": 320 },
{ "user_id": "jsmith", "entity_name": "Branch Office", "total_risk": 6200, "alert_count": 180 }
],
"pagination": {
"total": 85,
"limit": 50,
"offset": 0,
"has_more": true
},
"timestamp": "2026-03-20T10:30:00Z"
}
Top Risky Computers (legacy)
GET /api/v1/feed/risks/top-computers
Returns computers ranked by cumulative alert risk.
Prefer /risks/actors?actor_type=host
As with /risks/top-users, this sums alert risk rather than using the
platform's computed score. See /risks/actors.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{ "computer_name": "DC01", "entity_name": "Primary Site", "total_risk": 24000, "alert_count": 890 },
{ "computer_name": "WS-PC42", "entity_name": "Primary Site", "total_risk": 18000, "alert_count": 650 },
{ "computer_name": "EXCH01", "entity_name": "Branch Office", "total_risk": 9500, "alert_count": 420 }
],
"pagination": {
"total": 42,
"limit": 50,
"offset": 0,
"has_more": false
},
"timestamp": "2026-03-20T10:30:00Z"
}
Top Triggered Rules
GET /api/v1/feed/risks/top-rules
Returns the most frequently triggered detection rules.
Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_name |
string | (Optional) Filter by entity/site. Master keys only — scoped keys ignore this. |
limit |
integer | Results per page (default: 50, max: 500) |
offset |
integer | Pagination offset (default: 0) |
Response:
{
"success": true,
"data": [
{ "title": "Remote Access Tool Execution", "rule_level": "high", "count": 4521, "description": "Detects execution of known remote access tools" },
{ "title": "Suspicious PowerShell Command", "rule_level": "medium", "count": 3102, "description": "Detects suspicious PowerShell command line patterns" },
{ "title": "Mimikatz Command Line Usage", "rule_level": "critical", "count": 127, "description": "Detects Mimikatz command line arguments" }
],
"pagination": {
"total": 156,
"limit": 50,
"offset": 0,
"has_more": true
},
"timestamp": "2026-03-20T10:30:00Z"
}
Entity Filtering (Master Keys)
Master keys (entity_name=*) can filter by entity on any endpoint:
# All entities
curl -H "X-API-Key: smk_master_key..." \
"https://rhythmx.example.com/api/v1/feed/incidents"
# Specific entity
curl -H "X-API-Key: smk_master_key..." \
"https://rhythmx.example.com/api/v1/feed/incidents?entity_name=Primary%20Site"
# Scoped keys ignore the entity_name parameter
curl -H "X-API-Key: smk_customer_key..." \
"https://rhythmx.example.com/api/v1/feed/incidents"
# ^ Always returns only that customer's data
?entity_name= is accepted on every list, metrics, MITRE and risk endpoint. It is
not a security boundary — see Key Types.
Entity name matching is case-insensitive. PRIMARY SITE, Primary Site and
primary site all select the same entity, so you do not need to know the stored
casing to filter.
Normalise casing when consuming responses
The entity_name returned in a response reflects how it is stored, and that
differs by data source: incident and entity records return the canonical form,
while alert, case and risk records return it lowercased. Compare entity names
case-insensitively rather than with an exact string match, or records for one
entity may appear to belong to two.
Use /entities to discover valid entity names.
Error Codes
| Code | Meaning |
|---|---|
200 |
Success |
400 |
Bad request (missing required parameter) |
401 |
Invalid, expired, or disabled API key |
403 |
Entity not permitted for this key (see Entity Summary) |
404 |
Resource not found, or the resource belongs to another entity |
429 |
Rate limit exceeded |
500 |
Internal server error |
Error Response:
{
"error": "Invalid or disabled API key"
}
404 on another entity's resource
Fetching an incident, case or alert by ID that belongs to a different entity
returns 404, not 403 — the record is simply not visible to that key. A 404
therefore means "not found for you", which is intentional: it avoids confirming
that an ID exists under another entity.
Quick Reference
| Endpoint | Method | Description |
|---|---|---|
/api/v1/feed/health |
GET | Service health check |
/api/v1/feed/incidents |
GET | List incidents |
/api/v1/feed/incidents/{id} |
GET | Incident detail |
/api/v1/feed/incidents/{id}/related |
GET | Incident + related LogRhythm alarms, Sigma alerts & cases |
/api/v1/feed/cases |
GET | List threat cases |
/api/v1/feed/cases/{id} |
GET | Case detail |
/api/v1/feed/alerts |
GET | List RhythmX alerts |
/api/v1/feed/alerts/{id} |
GET | Alert detail |
/api/v1/feed/alerts/stats |
GET | Alert statistics |
/api/v1/feed/metrics/overview |
GET | SOC overview metrics |
/api/v1/feed/metrics/severity |
GET | Severity/status breakdown |
/api/v1/feed/metrics/mttr |
GET | Mean time to respond |
/api/v1/feed/metrics/trends |
GET | Daily incident/case trends |
/api/v1/feed/metrics/incidents/severity |
GET | Incidents by severity & status |
/api/v1/feed/metrics/incidents/aging |
GET | Open-incident backlog by age |
/api/v1/feed/metrics/incidents/attack-types |
GET | Incidents by attack type / MITRE tactic |
/api/v1/feed/mitre/tactics |
GET | MITRE ATT&CK tactics |
/api/v1/feed/mitre/techniques |
GET | MITRE ATT&CK techniques |
/api/v1/feed/entities |
GET | List entities with counts |
/api/v1/feed/entities/risk |
GET | Per-entity risk roll-up |
/api/v1/feed/entities/{name}/summary |
GET | Entity detail summary |
/api/v1/feed/risks/actors |
GET | Top risky actors by computed risk score |
/api/v1/feed/risks/actors/{actor}/activity |
GET | All activity observed for one actor |
/api/v1/feed/risks/top-users |
GET | Top users by cumulative alert risk (legacy) |
/api/v1/feed/risks/top-computers |
GET | Top computers by cumulative alert risk (legacy) |
/api/v1/feed/risks/top-rules |
GET | Top triggered rules |
API Key Management
API keys are managed by administrators through the Integration Admin interface in System Settings.
| Field | Description |
|---|---|
| Label | Human-readable label for the key |
| Description | Purpose of the key |
| Entity Scope | Required. Either a single entity, or All entities (master key) |
| Scopes | Recorded intent — not currently enforced |
| Rate Limit | Recorded per key — not currently applied (see Rate Limiting) |
| Expires At | Optional expiration date |
| Enabled | Whether the key is active |
Choosing an entity scope
Entity scope is a deliberate choice at creation and cannot be left blank:
- Single entity — the key reads only that entity and cannot be re-targeted. Use this for a customer, a customer's SOC, or any third-party consumer.
- All entities (master key) — stores
*. The key reads every entity and may narrow individual requests with?entity_name=. Use this only for your own aggregation systems; anyone holding it can read all tenants.
A single-entity key must name the entity exactly as it appears on incidents
(matching is case-insensitive). A name that matches no entity is not rejected — the
key authenticates normally and simply returns empty results, which is a common cause
of "my key returns nothing". Confirm the name against /entities
before issuing the key.
Attempting to create a key without an entity scope returns:
{
"error": "entity_name is required",
"detail": "Set entity_name to the entity this key may read, or to '*' to create a master key that can read all entities and be narrowed per-request with ?entity_name=."
}
Re-scoping an existing key
A key's entity scope can be changed in place, which keeps the existing secret valid — useful for correcting a key's scope without coordinating a rotation with whoever is using it:
curl -X PATCH -H "Authorization: Bearer <admin-jwt>" \
-H "Content-Type: application/json" \
-d '{"entity_name": "CustomerA"}' \
"https://rhythmx.example.com/api/api-keys/<key_id>"
The same rule applies — a blank value is rejected. Use * to widen a key to all
entities, or an entity name to restrict it.