Guides
SIEM export
Send the evidence ledger to Splunk, Sentinel, Chronicle or any SIEM as OCSF 1.3, or to a tracing backend as OpenTelemetry. Pull it on a schedule, or have it pushed by signed webhook.
The ledger is the source of truth; exports are views of it. Both formats carry the same subject and trace ids the ledger does, so an event in your SIEM can be traced back to the chained, signed record.
#Pull: OCSF
GET /api/w/:wid/export/ocsf returns NDJSON, one OCSF 1.3 event per line, for readers with evidence rights.
curl "https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/export/ocsf?since=2026-10-01T00:00:00Z&limit=5000" \
-H "cookie: __Host-sid=$IMMISCIBLE_SESSION" > immiscible.ndjson| Parameter | Meaning |
|---|---|
since, until | ISO 8601 times bounding the records |
limit | how many records at most |
#OpenTelemetry
GET /api/w/:wid/export/otel returns OTLP/JSON logs, ready to post to a collector’s /v1/logs. Records keep their traceparent, so they land on the same traces as your own spans.
#Push: signed webhooks
For near real time, register a webhook. Owners and admins create them; the secret is shown once.
curl -X POST "https://immiscible.fly.dev/api/w/$IMMISCIBLE_WORKSPACE/webhooks" \
-H "cookie: __Host-sid=$IMMISCIBLE_SESSION" -H "x-immiscible-csrf: 1" \
-H "content-type: application/json" \
-d '{ "url": "https://soar.example.com/hooks/immiscible", "events": ["agent_decision", "agent_freeze", "agent_incident"], "format": "ocsf" }'{
"id": "whk_3d2e",
"url": "https://soar.example.com/hooks/immiscible",
"events": ["agent_decision", "agent_freeze", "agent_incident"],
"format": "ocsf",
"secret": "whsec_...",
"signature": { "header": "immiscible-signature", "scheme": "t=<unix seconds>,v1=<hex HMAC-SHA256 of \"t.body\">" }
}eventsis a list of ledger kinds, or["*"]for everything. A kind the ledger never writes is refused with400 invalid_events, naming the closest real one (“did you mean agent_freeze?”). The kinds an agent’s life writes areagent_decision,agent_approval,agent_freeze,agent_incident,agent_settlementandmandate_change.formatisocsf(the default) ornative(the ledger record as it stands).- Up to five attempts with backoff. Private and internal addresses are refused.
POST .../webhooks/:id/testsends one now;GET .../deliveriesshows what was sent (a SHA-256 of the body) and what came back (the status and the first 1 KB), andPOST .../deliveries/:did/resendsends one again.
#Verifying a delivery
Each delivery carries immiscible-signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of <t>.<raw body> with the webhook’s secret. Check it before trusting the body, and refuse old timestamps:
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyImmiscible(rawBody: string, header: string, secret: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(',').map((kv) => kv.split('=', 2)));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? '', 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib
import hmac
import time
def verify_immiscible(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(kv.split("=", 1) for kv in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))#From a SOAR
A playbook that reads events from your SIEM usually wants to act too: freeze an agent, list waiting approvals, pull the evidence bundle. Give it a service token with exactly the scopes it needs, and read the kill switch for the safe way to freeze many agents.