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/status

Authentication

WhoHow
AnyoneGET /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
PeopleAuthorization: Bearer <session token> (from a code, SSO or LDAP), limited by their role: 403 names the role a call needs (Roles)
AutomationAuthorization: Bearer <admin token>: owner rights, attributed to admin-token
Components, SIEMsPOST /v1/events and POST /v1/alerts/<source> with Authorization: Bearer <ingest token>
Other installsPOST /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).

StatusWhen
400Bad JSON or a missing field (named); bad_query for a query that can't be read
401No session or token
403Your role can't do this: the message names the role needed
404No such resource
409Managed in a file, managed by the Keychain (keychain_managed), a code mismatch, or not possible now (with why)
503Not operating yet: {"error": "not operating yet", "mode": …}

Status and health

CallWhoAnswer
GET /healthanyone{"status": "ok"} (the container's health check runs 3am-core health instead)
GET /v1/statusanyoneVersion, 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]viewer3AM Doctor: status and every check with detail and hint (cached 15 s)
GET /v1/liveviewerMemory only, cheap: {"seq", "running": [incident, service, alerts, status, step, t_open]}. Poll it; refetch when seq moves
GET /v1/live
{"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

CallWhoPurpose
GET /v1/episodesviewerClosed incidents plus running and seq; paging, facets, search (below)
GET /v1/episodes?group=incidentviewerOne row per incident (its root episode, with related, related_ids, services)
GET /v1/episodes?scorecard=1viewerAdds scorecard over exactly the rows the filters select
GET /v1/episodes/<id>[?after_seq=N]viewerOne 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>viewerIncidents themselves: lifecycle, episodes, causes, fixes, history
POST /v1/episodes/<id>/team {"verdict","detail"?}responderWhat your team did: same, advice, other (with detail), none
QueryMeaning
limit, offset, sort (-t_open)Paging and sorting
qText: id, service, alerts, causes, the note, the fix, the diagnosis
queryThe query language
latestOnly the newest N (≤ 5000); the answer says latest and complete
range24h, 7d, 30d, 90d, 1y, all (since wins)
since, untilISO timestamp or date, inclusive / exclusive
service, decision, outcome, confidence, kind, resultFacets, comma-separated; each facet's counts apply every filter but its own
confirmed=yes|noWith or without a confirmed cause
histogram=dayPer-day totals (failures, symptoms, proven, mitigated, escalated)
GET /v1/episodes?limit=1
{"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"}]}
FieldValues
confidenceproven, linked, suspected, disputed, unproven, cleared
decisionact, 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)
outcomemitigated, unresolved, action_failed, rejected, escalated, explained, cleared, shadow, abandoned
resultresolved, restored, handed_over, linked, cleared, shadow, other
actionverified, 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).

CallPurpose
GET /v1/search/fieldsEach list's fields for the query language

query= works on /v1/episodes, /v1/approvals and /v1/ledger: Query language.

Approvals

CallWhoPurpose
GET /v1/approvalsviewerRequests with decisions and channels; facets status, risk; q, query, since, until, sort
POST /v1/approvals/<request>/withdrawresponderWithdraw a pending request (404 when none)

Autonomy, fix mode, halt, policy

CallWhoPurpose
GET /v1/autonomyviewerDefault and per-service levels, halt, the raise quorum, why raising is impossible now; fix_mode, drafted_quorum
POST /v1/autonomy {"service","level"}adminLowering 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"}adminadvise applies at once; draft is requested (202) or refused (409)
POST /v1/halt {"reason"}responderRefuse every action now
POST /v1/resumeadminResume
GET/PUT /v1/policyviewer / adminThe policy (Configuration files); a file-managed policy can't be edited

Connections and discovery

CallWhoPurpose
GET /v1/connectors/catalogueviewerEvery connector kind with its settings
GET /v1/connectorsviewerConfigured connectors, secrets masked, each with its load problem
PUT/DELETE /v1/connectors/<name>adminSave (secrets to 0600 files) / remove; the runtime rebuilds
POST /v1/connectors/testadminA connector's health check before saving
POST /v1/connectors/ssh/keypair {"name"}admin3AM's SSH key: the public key and the authorized_keys line (restricted to 3am-gate)
POST /v1/connectors/ssh/hostkey {"host","port"?}adminScan a host key, to pin it
POST /v1/discoveradminRebuild the estate (a job: GET /v1/jobs/<id>)

Estate and Study

CallWhoPurpose
GET /v1/estateviewerServices with every link and how it was made; found per connector (ok, stale, failed), problems, candidates, unlinked
GET/PUT /v1/estate/overridesviewer / adminServices added or edited by hand, and placed links
GET /v1/estate/suggestions, POST /v1/estate/suggestions/confirm|dismissviewer / adminInferred dependencies to confirm
POST /v1/study {"minutes"?}adminStart a study (15 min default, 120 max): 202, or 409 when one is running
POST /v1/study/stopadminStop before the next cause
GET /v1/studyviewerrunning (step, service, cause, done, of) and last (per service, obstacles, baseline, relations)

Impact and the lookback

CallWhoPurpose
GET/PUT /v1/impactadminYour figures: currency, engineer_hour, triage_min, fix_min, manual_resolve_min, downtime_hour, callout, annual_cost (0: not counted)
GET /v1/lookbackadminThe last report, and the connections that keep history
POST /v1/lookback {"days"}adminRead 7–365 days of resolved incidents from ServiceNow and PagerDuty; counts only. 409 when nothing keeps history
GET /v1/digest[?days=N]viewerThe digest: JSON plus Markdown

Licence and installs

CallWhoPurpose
POST /v1/licence {"licence"}ownerInstall (409 when a file is mounted)
DELETE /v1/licenceownerRemove (back to setup mode)
POST /v1/licence/checkownerCheck with Warden now (audited here and in Warden)
POST /v1/licence/redeem {"code"}ownerRedeem a trial code
PUT /v1/licence/services {"pinned"}ownerServices always covered
GET /v1/licence/usageadminThe signed usage statement
GET /v1/licence/installsviewerThis install, the licence's installs, the history (log), the tier's limit, a candidate, outgoing requests; can
POST /v1/licence/installs/ask {"purpose","label"?}adminOn a new install: {code, text}
POST /v1/licence/installs/look {"text"}adminWhat a pasted request asks, checked, with its digits
POST /v1/licence/installs/vouch {"text","code"}adminVouch: granted, pending (with why) or manual; 409 code_mismatch
POST /v1/licence/installs/polladminOn the new install: ask Warden where its request stands
POST /v1/licence/installs/remove {"fingerprint"}adminTake an install off

Keychain

CallRoleWhat it does
GET /v1/keychainviewerThis install's role, what it carries, members and their health, site keys
POST /v1/keychain/home {"label"}adminMake this install the home
POST /v1/keychain/site-keys {"labels","purpose","licence","hours"}adminMake site keys, shown once
POST /v1/keychain/site-keys/revoke {"id"}adminWithdraw a site key
POST /v1/keychain/enrol {"address","key"}adminSet this install up with a site key
POST /v1/keychain/windowadminOpen pairing for 10 minutes
POST /v1/keychain/members/confirm {"fingerprint","code"}, /members/removeadminPair a member, or remove one
POST /v1/keychain/join {"address","label"}, /join/confirm {"code"}, /join/canceladminPair with a home
POST /v1/keychain/joins/approve {"request"}adminAdd a paired member to the licence
POST /v1/keychain/scope {"skip"}adminOn the home: the parts not carried
POST /v1/keychain/own {"part","own"}adminOn a member: keep its own copy of a part
POST /v1/keychain/rename {"label","fingerprint"?}adminRename this install, or a member
POST /v1/keychain/handover {"fingerprint"}, /handover/cancel, /handover/accept {"code"}adminMove the home
POST /v1/keychain/take-overownerTake over as the home, after 15 minutes of silence
POST /v1/keychain/pull, /leaveadminSync now, or leave
POST /v1/keychain/seal {"passphrase"}, /unseal {"file","passphrase"}adminThe sealed file

Parts: licence, connections, mail, signin, people, policy, branding, numbers.

Access

CallWhoPurpose
POST /v1/auth/request, /verify, /password, /logoutanyone / signed inCodes, passwords, sign out
POST /v1/login {token}, POST /v1/auth/bootstrapanyone / admin tokenThe first owner; break-glass
GET /v1/auth/sso/start, /callback, POST /v1/auth/sso/redeem, POST /v1/auth/ldapanyoneSingle sign-on
GET/PUT /v1/mesigned inYou; your name and handles
GET/POST /v1/users, PUT/DELETE /v1/users/<id>adminPeople
GET /v1/teams, POST/PUT/DELETE /v1/teams[/<id>]viewer / admin or leadTeams
GET/PUT /v1/settings/mail, POST /v1/settings/mail/testadminMail
GET/PUT /v1/settings/accessadminWho may sign in
GET/PUT /v1/settings/sso, GET /v1/settings/sso/groups?q=, POST /v1/settings/sso/discover|test|syncadminSingle sign-on

Details: Sign-in, SSO, Roles.

Learning and branding

CallWhoPurpose
GET /v1/learningviewerCounts, precision, demotions, calibration
POST /v1/learning/label {"episode","correct","source"}responderA label from outside (an eval's ground truth)
GET /v1/learned, POST /v1/learned/<id> {"action"}viewer / adminPatterns; promote, reject, disable, enable, delete
GET /v1/branding (public), PUT /v1/brandinganyone / adminName, logos (data URIs, ≤ 500 KB), theme, accent, support links

The audit log

CallWhoPurpose
GET /v1/ledgerviewerNewest 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/verifyviewerThe hash chain, sequence, segment digests and seals
Incremental export
curl -s -H "Authorization: Bearer $TOKEN" "$THREEAM/v1/ledger?order=asc&after_seq=48000&limit=2000"

Events and alerts (ingest token)

CallPurpose
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"}}'

On this page