HTTP API
Every endpoint of 3AM's API (status, incidents, search, approvals, autonomy, policy, connections, estate, study, impact, lookback, licence, installs, Keychain, access, ledger, events, alerts) with who may call it and examples.
Everything the console does goes through this API, so anything you can click you can script. It is served on the console's port (8700). JSON in and out.
export THREEAM=http://127.0.0.1:8700
export TOKEN=… # a session token (sign in), or the admin token for automation
curl -s -H "Authorization: Bearer $TOKEN" $THREEAM/v1/statusAuthentication
| Who | How |
|---|---|
| Anyone | GET /health, GET /v1/status, GET /v1/branding, POST /v1/auth/request, /v1/auth/verify, /v1/auth/password, /v1/auth/ldap, /v1/login, the SSO flow |
| People | Authorization: Bearer <session token> (from a code, SSO or LDAP), limited by their role: 403 names the role a call needs (Roles) |
| Automation | Authorization: Bearer <admin token>: owner rights, attributed to admin-token |
| Components, SIEMs | POST /v1/events and POST /v1/alerts/<source> with Authorization: Bearer <ingest token> |
| Other installs | POST /v1/keychain/peer/*: no session; authenticated by pinned keys, throttled per address, 64 KB max |
A wrong code or token waits 0.5 s. Changes are attributed to the signed-in person (a by in the body is used only
with the admin token).
| Status | When |
|---|---|
400 | Bad JSON or a missing field (named); bad_query for a query that can't be read |
401 | No session or token |
403 | Your role can't do this: the message names the role needed |
404 | No such resource |
409 | Managed in a file, managed by the Keychain (keychain_managed), a code mismatch, or not possible now (with why) |
503 | Not operating yet: {"error": "not operating yet", "mode": …} |
Status and health
| Call | Who | Answer |
|---|---|---|
GET /health | anyone | {"status": "ok"} (the container's health check runs 3am-core health instead) |
GET /v1/status | anyone | Version, fingerprint, mode (setup, configuring, operating), licence (with services, installs, install_limit), setup progress, impact set, auth (sso), ledger head, models |
GET /v1/doctor[?fresh=1] | viewer | 3AM Doctor: status and every check with detail and hint (cached 15 s) |
GET /v1/live | viewer | Memory only, cheap: {"seq", "running": [incident, service, alerts, status, step, t_open]}. Poll it; refetch when seq moves |
{"seq": 48211, "running": [{"episode_id": "ep-20261007T091140-3be2f1", "service": "core-banking",
"alerts": ["FineractWriteFailures"], "status": "investigating", "step": "widen", "t_open": "2026-10-07T09:11:40Z"}]}Incidents
| Call | Who | Purpose |
|---|---|---|
GET /v1/episodes | viewer | Closed incidents plus running and seq; paging, facets, search (below) |
GET /v1/episodes?group=incident | viewer | One row per incident (its root episode, with related, related_ids, services) |
GET /v1/episodes?scorecard=1 | viewer | Adds scorecard over exactly the rows the filters select |
GET /v1/episodes/<id>[?after_seq=N] | viewer | One incident: closed record, or a live view (live: true, status, step, findings, leading hypotheses). after_seq returns only newer timeline events (merge by seq) |
GET /v1/incidents[?status=open,mitigating,monitoring,resolved], GET /v1/incidents/<id> | viewer | Incidents themselves: lifecycle, episodes, causes, fixes, history |
POST /v1/episodes/<id>/team {"verdict","detail"?} | responder | What your team did: same, advice, other (with detail), none |
| Query | Meaning |
|---|---|
limit, offset, sort (-t_open) | Paging and sorting |
q | Text: id, service, alerts, causes, the note, the fix, the diagnosis |
query | The query language |
latest | Only the newest N (≤ 5000); the answer says latest and complete |
range | 24h, 7d, 30d, 90d, 1y, all (since wins) |
since, until | ISO timestamp or date, inclusive / exclusive |
service, decision, outcome, confidence, kind, result | Facets, comma-separated; each facet's counts apply every filter but its own |
confirmed=yes|no | With or without a confirmed cause |
histogram=day | Per-day totals (failures, symptoms, proven, mitigated, escalated) |
{"episodes": [{"episode_id": "ep-20261004T175412-592a91", "service": "platform", "t_open": "2026-10-04T17:54:12Z",
"t_close": "2026-10-04T17:57:32Z", "alerts": ["DependencyPortUnreachable"], "confirmed": [], "decision": "abstain",
"outcome": "escalated", "result": "handed_over", "action": "", "handover_s": 90, "autonomy": "shadow"}]}| Field | Values |
|---|---|
confidence | proven, linked, suspected, disputed, unproven, cleared |
decision | act, shadow, restore, mitigated_first, ask_human, vetoed, same_cause, downstream, related, settled, recurrence, abstain, cleared, denied, dry_run_failed, no_approval_route, change_blocked, blocked (Incidents) |
outcome | mitigated, unresolved, action_failed, rejected, escalated, explained, cleared, shadow, abandoned |
result | resolved, restored, handed_over, linked, cleared, shadow, other |
action | verified, taken_back, not_verified, or empty |
The scorecard: counts per result, share_ended_by_3am, restore_s.median, handover_s (median, within_90),
actions (ran, verified, taken_back, not_verified), minutes_saved (only from your own figures; null
without them).
Search
| Call | Purpose |
|---|---|
GET /v1/search/fields | Each list's fields for the query language |
query= works on /v1/episodes, /v1/approvals and /v1/ledger: Query language.
Approvals
| Call | Who | Purpose |
|---|---|---|
GET /v1/approvals | viewer | Requests with decisions and channels; facets status, risk; q, query, since, until, sort |
POST /v1/approvals/<request>/withdraw | responder | Withdraw a pending request (404 when none) |
Autonomy, fix mode, halt, policy
| Call | Who | Purpose |
|---|---|---|
GET /v1/autonomy | viewer | Default and per-service levels, halt, the raise quorum, why raising is impossible now; fix_mode, drafted_quorum |
POST /v1/autonomy {"service","level"} | admin | Lowering applies at once (200). Raising (L1, L2) is requested (202), applied when the quorum approves; 409 with why when it can't be |
POST /v1/fix-mode {"mode"} | admin | advise applies at once; draft is requested (202) or refused (409) |
POST /v1/halt {"reason"} | responder | Refuse every action now |
POST /v1/resume | admin | Resume |
GET/PUT /v1/policy | viewer / admin | The policy (Configuration files); a file-managed policy can't be edited |
Connections and discovery
| Call | Who | Purpose |
|---|---|---|
GET /v1/connectors/catalogue | viewer | Every connector kind with its settings |
GET /v1/connectors | viewer | Configured connectors, secrets masked, each with its load problem |
PUT/DELETE /v1/connectors/<name> | admin | Save (secrets to 0600 files) / remove; the runtime rebuilds |
POST /v1/connectors/test | admin | A connector's health check before saving |
POST /v1/connectors/ssh/keypair {"name"} | admin | 3AM's SSH key: the public key and the authorized_keys line (restricted to 3am-gate) |
POST /v1/connectors/ssh/hostkey {"host","port"?} | admin | Scan a host key, to pin it |
POST /v1/discover | admin | Rebuild the estate (a job: GET /v1/jobs/<id>) |
Estate and Study
| Call | Who | Purpose |
|---|---|---|
GET /v1/estate | viewer | Services with every link and how it was made; found per connector (ok, stale, failed), problems, candidates, unlinked |
GET/PUT /v1/estate/overrides | viewer / admin | Services added or edited by hand, and placed links |
GET /v1/estate/suggestions, POST /v1/estate/suggestions/confirm|dismiss | viewer / admin | Inferred dependencies to confirm |
POST /v1/study {"minutes"?} | admin | Start a study (15 min default, 120 max): 202, or 409 when one is running |
POST /v1/study/stop | admin | Stop before the next cause |
GET /v1/study | viewer | running (step, service, cause, done, of) and last (per service, obstacles, baseline, relations) |
Impact and the lookback
| Call | Who | Purpose |
|---|---|---|
GET/PUT /v1/impact | admin | Your figures: currency, engineer_hour, triage_min, fix_min, manual_resolve_min, downtime_hour, callout, annual_cost (0: not counted) |
GET /v1/lookback | admin | The last report, and the connections that keep history |
POST /v1/lookback {"days"} | admin | Read 7–365 days of resolved incidents from ServiceNow and PagerDuty; counts only. 409 when nothing keeps history |
GET /v1/digest[?days=N] | viewer | The digest: JSON plus Markdown |
Licence and installs
| Call | Who | Purpose |
|---|---|---|
POST /v1/licence {"licence"} | owner | Install (409 when a file is mounted) |
DELETE /v1/licence | owner | Remove (back to setup mode) |
POST /v1/licence/check | owner | Check with Warden now (audited here and in Warden) |
POST /v1/licence/redeem {"code"} | owner | Redeem a trial code |
PUT /v1/licence/services {"pinned"} | owner | Services always covered |
GET /v1/licence/usage | admin | The signed usage statement |
GET /v1/licence/installs | viewer | This install, the licence's installs, the history (log), the tier's limit, a candidate, outgoing requests; can |
POST /v1/licence/installs/ask {"purpose","label"?} | admin | On a new install: {code, text} |
POST /v1/licence/installs/look {"text"} | admin | What a pasted request asks, checked, with its digits |
POST /v1/licence/installs/vouch {"text","code"} | admin | Vouch: granted, pending (with why) or manual; 409 code_mismatch |
POST /v1/licence/installs/poll | admin | On the new install: ask Warden where its request stands |
POST /v1/licence/installs/remove {"fingerprint"} | admin | Take an install off |
Keychain
| Call | Role | What it does |
|---|---|---|
GET /v1/keychain | viewer | This install's role, what it carries, members and their health, site keys |
POST /v1/keychain/home {"label"} | admin | Make this install the home |
POST /v1/keychain/site-keys {"labels","purpose","licence","hours"} | admin | Make site keys, shown once |
POST /v1/keychain/site-keys/revoke {"id"} | admin | Withdraw a site key |
POST /v1/keychain/enrol {"address","key"} | admin | Set this install up with a site key |
POST /v1/keychain/window | admin | Open pairing for 10 minutes |
POST /v1/keychain/members/confirm {"fingerprint","code"}, /members/remove | admin | Pair a member, or remove one |
POST /v1/keychain/join {"address","label"}, /join/confirm {"code"}, /join/cancel | admin | Pair with a home |
POST /v1/keychain/joins/approve {"request"} | admin | Add a paired member to the licence |
POST /v1/keychain/scope {"skip"} | admin | On the home: the parts not carried |
POST /v1/keychain/own {"part","own"} | admin | On a member: keep its own copy of a part |
POST /v1/keychain/rename {"label","fingerprint"?} | admin | Rename this install, or a member |
POST /v1/keychain/handover {"fingerprint"}, /handover/cancel, /handover/accept {"code"} | admin | Move the home |
POST /v1/keychain/take-over | owner | Take over as the home, after 15 minutes of silence |
POST /v1/keychain/pull, /leave | admin | Sync now, or leave |
POST /v1/keychain/seal {"passphrase"}, /unseal {"file","passphrase"} | admin | The sealed file |
Parts: licence, connections, mail, signin, people, policy, branding, numbers.
Access
| Call | Who | Purpose |
|---|---|---|
POST /v1/auth/request, /verify, /password, /logout | anyone / signed in | Codes, passwords, sign out |
POST /v1/login {token}, POST /v1/auth/bootstrap | anyone / admin token | The first owner; break-glass |
GET /v1/auth/sso/start, /callback, POST /v1/auth/sso/redeem, POST /v1/auth/ldap | anyone | Single sign-on |
GET/PUT /v1/me | signed in | You; your name and handles |
GET/POST /v1/users, PUT/DELETE /v1/users/<id> | admin | People |
GET /v1/teams, POST/PUT/DELETE /v1/teams[/<id>] | viewer / admin or lead | Teams |
GET/PUT /v1/settings/mail, POST /v1/settings/mail/test | admin | |
GET/PUT /v1/settings/access | admin | Who may sign in |
GET/PUT /v1/settings/sso, GET /v1/settings/sso/groups?q=, POST /v1/settings/sso/discover|test|sync | admin | Single sign-on |
Learning and branding
| Call | Who | Purpose |
|---|---|---|
GET /v1/learning | viewer | Counts, precision, demotions, calibration |
POST /v1/learning/label {"episode","correct","source"} | responder | A label from outside (an eval's ground truth) |
GET /v1/learned, POST /v1/learned/<id> {"action"} | viewer / admin | Patterns; promote, reject, disable, enable, delete |
GET /v1/branding (public), PUT /v1/branding | anyone / admin | Name, logos (data URIs, ≤ 500 KB), theme, accent, support links |
The audit log
| Call | Who | Purpose |
|---|---|---|
GET /v1/ledger | viewer | Newest first unless order=asc; limit (≤ 2000), offset, type (prefixes), trace, actor, q, query, since, until, before_seq, after_seq (incremental export), latest; counts per category |
GET /v1/ledger/verify | viewer | The hash chain, sequence, segment digests and seals |
curl -s -H "Authorization: Bearer $TOKEN" "$THREEAM/v1/ledger?order=asc&after_seq=48000&limit=2000"Events and alerts (ingest token)
| Call | Purpose |
|---|---|
POST /v1/events {"type","actor","data"?,"reason"?,"trace_id"?} | Append an event; type starts signal., retrieval., model., check., decision., approval., action., outcome., config., connector., system. or human. |
POST /v1/alerts/<source> | Any monitoring tool's or SIEM's webhook: 202 {"accepted","resolved","signals"}, 400 when no normaliser can read it (Alert webhooks) |
curl -s -X POST $THREEAM/v1/events -H "Authorization: Bearer $INGEST_TOKEN" -H 'Content-Type: application/json' \
-d '{"type":"human.action","actor":"human:dba-oncall","data":{"service":"core-banking-db","action":"sql.write","detail":"SET GLOBAL read_only = OFF"}}'Cheat sheet
Everything on one page. Install, upgrade, models, Doctor, CLI, the API calls you use most, Keychain, licence, keyboard shortcuts, query language, files and ports.
Query language
One line to search incidents, approvals and the audit log on the server, across years: fields, operators, wildcards, regular expressions, numbers, durations, dates and the time window.