API reference

REST. JSON in, JSON out. Bearer auth.

Base URL

All endpoints below are rooted at {PUBLIC_URL}/api/v1.

Authentication

Generate an API key under Settings > API keys in the UI. Send it as a Bearer token:

curl
curl -H "Authorization: Bearer $IR_API_KEY" https://ir.example.com/api/v1/alerts

Errors

Standard HTTP codes. Body is { "error": "code", "message": "human-readable", "details": {...} }.

CodeMeaning
400Validation error — see details.
401Missing or invalid bearer token.
403Token doesn't have the required scope/role.
404Entity doesn't exist (or you can't see it).
409Idempotency conflict.
423License grace period exceeded — org locked.
429Rate limit; retry after Retry-After seconds.

Alerts

POST /alerts — ingest an alert

request
{
  "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 /alerts

Filter: ?status=triage&severity=high&source=crowdstrike&cursor=.... Returns paginated list.

POST /alerts/{id}/dismiss

Body: { "reason_code": "known-good", "comment": "..." }.

POST /alerts/{id}/escalate

Body: { "case_id": null | "case_..." }. Null creates a new case carrying the alert's observables and severity.

Cases

POST /cases

Create 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}/comments

Body: { "body": "markdown supported", "mentions": ["user_..."] }.

POST /cases/{id}/attachments

Multipart upload. Max 50 MB by default (raise client_max_body_size in your proxy if needed).

POST /cases/{id}/resolve

Body: { "verdict": "true_positive" | "false_positive" | "benign" | "duplicate", "root_cause": "...", "narrative": "..." }. Generates the report.

Observables

GET /observables/search?type=ip&value=203.0.113.4

Find every alert and case touching this observable. Useful for "have we seen this IP before?" lookups from external tools.

POST /observables/suppress

Body: { "type": "domain", "value": "stats.example.com", "scope": "global" | "source:edr", "until": "2026-12-31T00:00:00Z" }.

Webhooks

Outbound webhooks fire on case transitions. Configure under Settings > Webhooks. Payload:

webhook body
{
  "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.

Healthcheck

GET /healthz returns {"status":"ok","db":"ok","ch":"ok","license":"company","grace_h":0}. Unauthenticated. Use it for your monitoring probes.