REST. JSON in, JSON out. Bearer auth.
All endpoints below are rooted at {PUBLIC_URL}/api/v1.
Generate an API key under Settings > API keys in the UI. Send it as a Bearer token:
curl -H "Authorization: Bearer $IR_API_KEY" https://ir.example.com/api/v1/alerts
Standard HTTP codes. Body is { "error": "code", "message": "human-readable", "details": {...} }.
| Code | Meaning |
|---|---|
400 | Validation error — see details. |
401 | Missing or invalid bearer token. |
403 | Token doesn't have the required scope/role. |
404 | Entity doesn't exist (or you can't see it). |
409 | Idempotency conflict. |
423 | License grace period exceeded — org locked. |
429 | Rate limit; retry after Retry-After seconds. |
POST /alerts — ingest an alert{
"source": "crowdstrike",
"external_id": "falcon-9182734",
"severity": "high",
"title": "Suspicious PowerShell on WS-042",
"detected_at": "2026-06-02T09:14:00Z",
"asset": { "hostname": "WS-042", "owner": "[email protected]" },
"observables": [
{ "type": "ip", "value": "203.0.113.4" },
{ "type": "hash", "value": "da39a3ee..." }
],
"raw": { /* original payload */ }
}
Idempotent on (source, external_id). Returns { "id": "alert_...", "deduplicated": false }.
GET /alertsFilter: ?status=triage&severity=high&source=crowdstrike&cursor=.... Returns paginated list.
POST /alerts/{id}/dismissBody: { "reason_code": "known-good", "comment": "..." }.
POST /alerts/{id}/escalateBody: { "case_id": null | "case_..." }. Null creates a new case carrying the alert's observables and severity.
POST /casesCreate a case directly (without escalating an alert). Same shape minus the alert-specific fields.
GET /cases/{id}Returns the full case payload including timeline, observables, evidence list, ATT&CK mapping.
POST /cases/{id}/commentsBody: { "body": "markdown supported", "mentions": ["user_..."] }.
POST /cases/{id}/attachmentsMultipart upload. Max 50 MB by default (raise client_max_body_size in your proxy if needed).
POST /cases/{id}/resolveBody: { "verdict": "true_positive" | "false_positive" | "benign" | "duplicate", "root_cause": "...", "narrative": "..." }. Generates the report.
GET /observables/search?type=ip&value=203.0.113.4Find every alert and case touching this observable. Useful for "have we seen this IP before?" lookups from external tools.
POST /observables/suppressBody: { "type": "domain", "value": "stats.example.com", "scope": "global" | "source:edr", "until": "2026-12-31T00:00:00Z" }.
Outbound webhooks fire on case transitions. Configure under Settings > Webhooks. Payload:
{
"event": "case.resolved",
"case_id": "case_01HZ...",
"verdict": "true_positive",
"at": "2026-06-02T11:42:00Z",
"by": "user_01HY..."
}
Signed with X-IR-Signature: sha256=<hex> using your webhook secret. Retried with exponential backoff up to 24 h.
GET /healthz returns {"status":"ok","db":"ok","ch":"ok","license":"company","grace_h":0}. Unauthenticated. Use it for your monitoring probes.