The SOAR profile of the API

The full API has more than two hundred operations. A connector exposing all of them is unusable: nobody scrolls a playbook menu looking for natProposalsDismiss. So fourteen operations carry the SOAR tag and the x-soar: true extension, each with a typed response schema and a response example, and they are published as their own document:

  • GET /api/openapi-soar.yaml — the same document, served by your own instance. Take it from there when you can: it describes the operations that daemon actually serves, and an air-gapped instance can hand it over.
  • openapi-soar.yaml — the fourteen. This is the file to import into your SOAR.
  • openapi.yaml — the complete reference.

The first is derived from the second automatically and committed; it is never edited by hand, so it cannot drift.

The fourteen

Authentication is a Bearer token throughout: Authorization: Bearer obs_….

#OperationCallPermissionUse it to
1lookupEnrichmentGET /api/enrichment/lookup?ip=…sessions:readEnrich up to 50 addresses in one call
2getHostContextGET /api/context/host?ip=…&window=…sessions:readSee everything one host did over a window
3getPeersPOST /api/context/peerssessions:readThe same, filtered by direction, scope and instant
4checkPolicyPOST /api/policy/checkrules:readAsk whether one flow was already allowed
5exportIndicatorsGET /api/indicatorsindicators:readPull the observed-address feed
6listAlertsGET /api/alertsalerts:readPoll or reconcile alerts
7getAlertGET /api/alerts/{id}alerts:readResume from an alert id, with its evidence
8setAlertStatusPOST /api/alerts/{id}/statusalerts:ackTake ownership once a ticket exists
9resolveIpGET /api/query/resolve-ip?ip=…sessions:readTurn an address into a catalogued asset
10getHostGET /api/carto/hosts/{name}cartography:readRead that asset’s interfaces, services, groups
11searchCartographyGET /api/carto/search?q=…cartography:readFind an entity by name
12findCoveringRulePOST /api/rules/find-coveringrules:readThe raw covering-rule lookup
13getStatusGET /api/statusmonitoring:readConfirm which instance answered, and that it is alive
14executeQueryPOST /api/querynfql:executeThe escape hatch: raw NFQL

The five that carry a playbook are worth spelling out.

1. lookupEnrichment

curl -sG https://obserae.corp.example/api/enrichment/lookup \
  -H "Authorization: Bearer obs_…" \
  --data-urlencode "ip=203.0.113.45" --data-urlencode "ip=10.0.0.50"

Returns {"items": [...]}, one entry per address in request order, each with scope, asn, as_org, country, cloud, rdns, threat_intel[] and carto{}. Repeat ip up to 50 times; a 51st is a 400 — the batch is refused, never silently truncated, because a playbook handed half its evidence decides on half its evidence.

2. getHostContext

curl -sG https://obserae.corp.example/api/context/host \
  -H "Authorization: Bearer obs_…" \
  --data-urlencode "ip=10.0.0.50" \
  --data-urlencode "window=24h" \
  --data-urlencode "as_of=2026-06-04T12:00:00Z"

Returns services_exposed[], talks_to[], talked_to_by[], volume{in_bytes,out_bytes} and new_peers_in_window[]. Every peer carries policy_covered — the flow matrix’s verdict on that exact conversation — which is what makes the answer a segmentation report rather than a traffic dump.

window is 1h, 24h or 7d, and nothing else: it is what bounds the scan, so an unrecognised value is refused rather than widened. as_of moves that window without widening it. Pass the alert’s fired_at and you see what the host was doing when it fired, not what it is doing now.

3. getPeers

{"ip": "10.0.0.50", "window": "24h", "direction": "out",
 "scope": "external", "limit": 100, "as_of": "2026-06-04T12:00:00Z"}

The same question, narrowed. direction is in, out or both; scope is internal, external or any. Peers come back largest first. limit above 1000 is clamped and truncated says so.

4. checkPolicy

{"src": "10.0.0.50", "dst": "10.20.4.11", "port": 5432, "protocol": "tcp"}
{"covered": false,
 "matched_rule": null,
 "nearest_rules": [{"name": "build-to-db", "why_not": "port not in rule"}],
 "src_entity": {"carto": {"host": "ci-runner-prod", "group": "build"}},
 "dst_entity": {"carto": {"host": "db-01", "network": "db"}}}

why_not is the field that turns a boolean into a ticket somebody can act on: “the rule build→db exists, but 5432 is not in it” is a sentence a network owner can answer. Both src and dst are required — a verdict about half a pair would answer a question nobody asked — and a reference that resolves to nothing comes back covered: false with a note, never silently allowed.

5. exportIndicators

curl -sD- https://obserae.corp.example/api/indicators?since=24h\&min_severity=high \
  -H "Authorization: Bearer obs_…" \
  -H "If-None-Match: \"$(cat last.etag)\""

format is plain (one address per line, # comments — what a firewall or an RPZ ingests directly), json, csv or misp.

Always send If-None-Match. The response carries ETag and Last-Modified, and an unchanged feed is answered 304 with no body. A consumer polling every five minutes without a conditional request forces a full recomputation twelve times an hour, forever, and the day someone points at the load it will be your recipe they find. Store the ETag, send it back, and act only on a 200.

A token for a playbook

Grant only what your playbooks call. sessions:read, alerts:read, alerts:ack, cartography:read and rules:read cover eleven of the fourteen. Add nfql:execute only if you use executeQuery, and monitoring:read only if you check instance health. None of those permits any change to the cartography, the rules or the configuration.

indicators:read is the exception, and it does not belong on the playbook token. Put it on the enforcement device’s own credential — the firewall, the RPZ, the blocklist that pulls the feed — so that revoking that device’s access does not revoke your playbooks’. It is in no built-in group but admin; create a group holding just it. See Authentication.

Every authenticated call to these operations is written to the audit log with its token, permission, operation, timestamp and status code — reads included, which is unusual and deliberate: an auditor asking who queried what about an incident gets an answer.