Central panel and agents
When SentrAI runs as a central panel, each protected machine runs an agent. A machine joins with a
one-time join code made on the panel and gets its own machine key (sa_...) in return. The
agent uses that key to send its events, report what runs on it, pick up jobs and report back after
working offline. The operator calls on this page are what the dashboard’s Machines page uses.
Every endpoint here needs the API token (see Signing in) unless it says otherwise. The ones marked agent also answer a machine key, for that machine only (see Machine keys).
Join a machine
POST /fleet/join-codes
Makes a one-time join code. It is shown only here, once: the panel keeps just a hash of it. A code
works for 24 hours and for one machine. Written to the audit trail (join_code_created).
Body
| Field | Type | Default | Meaning |
|---|---|---|---|
operator |
string | operator |
who made the code |
name |
string | none | the name the machine will get, instead of the one its agent sends (letters, digits, ., -, _, up to 63) |
{"operator": "erick", "name": "office-pc"}
Returns the code with what a new machine needs to reach the panel (see GET /fleet/panel).
{
"code": "VGT7-6ULJ-J3C4",
"expires_at": "2026-10-28T02:45:12+00:00",
"name": "office-pc",
"panel_url": "https://panel.example.com:8443",
"pin": "sha256:..."
}
Errors: 400 machine names use letters, digits, dot, dash and underscore (up to 63).
GET /fleet/panel
The address and certificate pin new machines use to reach this panel, from SENTRAI_PANEL_URL and
SENTRAI_PANEL_CERT. Both are empty strings when they are not set.
Returns: {"panel_url": "https://panel.example.com:8443", "pin": "sha256:..."}
POST /fleet/enrol
Open: no token needed. A new machine swaps its join code for a machine key. The installer does
this for you (install-agent.sh --code or install-agent.ps1). The code is used up. Written to the
audit trail (machine_joined).
Body
| Field | Type | Meaning |
|---|---|---|
code |
string, required | the join code; dashes and letter case do not matter |
host |
string, required | the machine’s name; ignored when the code fixed one |
{"code": "VGT7-6ULJ-J3C4", "host": "DESKTOP-7F3K"}
Returns
{
"agent_id": "7d21e8c6d783",
"host": "office-pc",
"key": "sa_7d21e8c6d783.<secret>",
"risk_scope": "fleet"
}
key is shown only here; keep it on the machine (the installer saves it readable by the agent only).
risk_scope says whether the risk index is counted per machine (host) or for the whole fleet (fleet).
Errors
| Status | detail |
|---|---|
400 |
machine names use letters, digits, dot, dash and underscore (up to 63) |
403 |
join code is wrong, used or expired; make a new one on the panel |
409 |
a machine named 'office-pc' has already joined; revoke it on the panel first or join with another --name |
429 |
too many wrong join codes; try again in a few minutes (after 10 wrong codes in 10 minutes) |
Manage machines
GET /fleet/agents
Every machine that has joined, revoked ones included.
Returns
[
{
"agent_id": "7d21e8c6d783",
"host": "office-pc",
"joined_at": "2026-10-27T02:45:12+00:00",
"invited_by": "erick",
"last_seen": "2026-10-27T02:50:01+00:00",
"revoked_at": null,
"revoked_by": null
}
]
A machine that has sent an offline report also has offline (its last report, as
POST /fleet/offline-report returns it) and offline_total (how many it has sent).
POST /fleet/agents/{agent_id}/revoke
Revokes a machine’s key at once: its next call gets 401
unknown or revoked machine key: join the panel again. Jobs still waiting for it are dropped. Written
to the audit trail (machine_revoked).
Body: {"operator": "erick"} (operator defaults to operator).
Returns the machine’s record, as in GET /fleet/agents, now with revoked_at and revoked_by.
Errors: 404 no machine with id 7d21e8c6d783.
GET /fleet/me
Agent. Which machine a key belongs to. Agents call it to check their key works.
Returns: {"agent_id": "7d21e8c6d783", "host": "office-pc", "risk_scope": "fleet"}
Errors: 400 only a machine's agent key has a /fleet/me (called with the API token).
POST /fleet/services
Agent. What runs on a machine: its agent reports this every minute, and the Machines page shows it next to the machine’s open incidents. Each report replaces the last one.
Query
| Name | Type | Meaning |
|---|---|---|
host |
string | the machine, when called with the API token; a machine key always reports for itself |
Body: services, a list of up to 200 entries.
| Field | Type | Meaning |
|---|---|---|
name |
string, required | the service, up to 64 characters |
port |
integer | the port it listens on, 0 to 65535 |
proto |
string | tcp or udp |
process |
string | the program’s name |
{"services": [{"name": "nginx", "port": 443, "proto": "tcp", "process": "nginx"},
{"name": "sshd", "port": 22, "proto": "tcp", "process": "sshd"}]}
Returns: {"ok": true, "services": 2}
Errors: 400 which machine? (?host=) (API token with no host).
POST /fleet/hardware
Agent. How a machine’s hardware is doing: its agent sends a reading every 30 seconds. The panel keeps the latest one and about an hour of history (120 readings) per machine, in memory, and shows them on the machine’s Hardware tab and in the machine list. Every part is optional: a machine without temperature sensors simply sends none.
A reading over a limit tells the on-duty person once, as a hardware alert, and again only after it has
come back 5 points under the limit. CPU, memory and temperature must stay over for SENTRAI_HW_FOR_MIN
minutes first; a disk counts at once. SentrAI only reports this: nothing is changed on the machine.
| Setting | Default | Meaning |
|---|---|---|
SENTRAI_HW_ALERTS |
on |
off: show the numbers, never send an alert |
SENTRAI_HW_CPU_PCT |
90 |
CPU use, % |
SENTRAI_HW_MEM_PCT |
90 |
memory use, % |
SENTRAI_HW_DISK_PCT |
90 |
any one disk’s use, % |
SENTRAI_HW_TEMP_C |
85 |
the hottest sensor, °C |
SENTRAI_HW_FOR_MIN |
5 |
minutes CPU, memory or heat must stay over the limit |
Query
| Name | Type | Meaning |
|---|---|---|
host |
string | the machine, when called with the API token; a machine key always reports for itself |
Body
| Field | Type | Meaning |
|---|---|---|
cpu |
object | percent (0 to 100), cores, load (1, 5 and 15 minute load average; not on Windows) |
memory, swap |
object | total and used in bytes, percent |
disks |
list, up to 32 | mount, total, used, percent for each real disk |
net |
object | sent and recv in bytes since boot, sent_rate and recv_rate in bytes per second |
temps |
list, up to 32 | label, current and the sensor’s own high mark, in °C |
uptime_s |
number | seconds since the machine started |
os |
string | the operating system and its version |
{"cpu": {"percent": 12.5, "cores": 4, "load": [0.42, 0.37, 0.30]},
"memory": {"total": 8589934592, "used": 3521873920, "percent": 41.0},
"disks": [{"mount": "/", "total": 107374182400, "used": 53687091200, "percent": 50.0}],
"net": {"sent": 81234567, "recv": 912345678, "sent_rate": 2048.0, "recv_rate": 15360.0},
"temps": [{"label": "coretemp Package id 0", "current": 54.0, "high": 100.0}],
"uptime_s": 273600, "os": "Linux 6.8.0"}
Returns: {"ok": true, "alerts": 0}, where alerts is how many limits this reading newly crossed.
Errors: 400 which machine? (?host=) (API token with no host); 422 for a value out of range.
GET /fleet/hardware
One machine’s latest hardware reading, its history for the charts, and the alert limits in use.
Query
| Name | Type | Meaning |
|---|---|---|
host |
string, required | the machine |
Returns
{
"host": "web-01",
"latest": {"cpu": {"percent": 12.5, "cores": 4}, "memory": {"percent": 41.0}, "disks": [],
"at": "2026-10-27T02:45:20+00:00", "warnings": ["disk / is 93% full (limit 90%)"]},
"history": [{"t": "2026-10-27T02:45:20+00:00", "cpu": 12.5, "memory": 41.0, "disk": 93.0, "temp": 54.0,
"net_in": 15360.0, "net_out": 2048.0}],
"limits": {"alerts": true, "cpu": 90.0, "memory": 90.0, "disk": 90.0, "temp": 85.0, "for_min": 5.0}
}
latest is the last reading as sent, plus at and warnings (the limits it is over right now); it is
null when the machine never sent one since the panel started. In history, disk is the fullest
disk and temp the hottest sensor.
Send a job to a machine
A job asks a machine to run a threat scan, take a backup of its own folders, check its packages for
known vulnerabilities (dep_scan), update one vulnerable package (dep_update) or undo such an update
(dep_rollback). The panel only queues it; the machine’s agent picks it up, runs it and reports the result. A job nobody picks up in 24 hours
is dropped, and one that never reports back within 3 hours is marked failed. The last 20 finished jobs
per machine are kept.
POST /fleet/jobs
Queues a job. Written to the audit trail (machine_job_queued).
Body
| Field | Type | Default | Meaning |
|---|---|---|---|
host |
string, required | a joined machine | |
kind |
string, required | threat_scan, backup, dep_scan, dep_update or dep_rollback |
|
operator |
string | operator |
who asked |
params |
object | dep_update: {"finding": "<id>"}, a finding of the machine’s latest dependency scan whose update is safe; dep_rollback: {"update": "u-<id>"}, an update made there. Other jobs take none |
A package update carries only that id. The agent looks the package, its place and the fixed version
up in its own last scan, so the panel cannot make a machine install anything else, and only safe
findings (a virtualenv package or an npm project’s direct dependency, fixed within the same major
version) are updated. The person who pressed Update is the approval; it is in the audit trail with
the package and both versions.
{"host": "office-pc", "kind": "dep_update", "operator": "erick", "params": {"finding": "3f1c9a0b7d2e4f61"}}
{"host": "office-pc", "kind": "threat_scan", "operator": "erick"}
Returns the job.
{
"id": "j-f7d2dc41",
"host": "office-pc",
"kind": "threat_scan",
"status": "queued",
"requested_by": "erick",
"created_at": "2026-10-27T02:45:12+00:00",
"started_at": null,
"finished_at": null,
"result": null,
"error": null,
"summary": "waiting for the machine to pick it up"
}
status is queued, running, done, failed or cancelled. summary says it in a sentence.
Errors
| Status | detail |
|---|---|
400 |
unknown job 'format_disk'; use one of: threat_scan, backup, dep_scan, dep_update, dep_rollback |
400 |
a package update needs 'finding': the id from the machine's own list |
404 |
no joined machine named 'nope' |
404 |
3f1c9a0b7d2e4f61 is not in office-pc's latest dependency scan |
409 |
pillow is not a one-press update: The fix (13.0.0) is a new major version; ... |
409 |
update u-1a2b3c4d was already undone |
409 |
a threat scan is already queued on office-pc (j-f7d2dc41) |
GET /fleet/jobs
Jobs, newest first.
Query
| Name | Type | Meaning |
|---|---|---|
host |
string, up to 63 characters | only this machine’s jobs |
Returns a list of jobs, as POST /fleet/jobs.
POST /fleet/jobs/{job_id}/cancel
Cancels a job that is still waiting. One the machine has started cannot be cancelled.
Body: {"operator": "erick", "justification": "wrong machine"} (justification is optional).
Returns the job, now cancelled, with error and summary cancelled by erick.
Errors: 404 no job j-2a2efdf4; 409 job j-2a2efdf4 is running; only a job still waiting can be cancelled.
GET /fleet/jobs/next
Agent. The next job for this machine, which is marked running. Agents ask every minute.
Returns: {"job": {"id": "j-f7d2dc41", "kind": "threat_scan"}}, or {"job": null} when there is none.
A package update or undo also has its params.
Errors: 400 only a machine's agent key picks up jobs.
POST /fleet/jobs/result
Agent. The machine reports how a job went. A threat scan’s findings raise “Malware found” alerts
on the panel, as a local threat scan does; a failed backup alerts the
person on duty. Written to the audit trail (machine_job_done or machine_job_failed).
Body
| Field | Type | Meaning |
|---|---|---|
id |
string, required | the job |
ok |
boolean, required | whether it worked |
result |
object | what the scan or backup returned (cleaned and size-limited on the panel) |
error |
string | why it failed, up to 2000 characters |
{
"id": "j-f7d2dc41",
"ok": true,
"result": {
"roots": ["~/site"],
"files_checked": 120,
"counts": {"critical": 1, "high": 0, "medium": 0},
"took_s": 0.8,
"findings": [{"path": "~/site/uploads/img.php", "line": 1, "kind": "web_shell", "severity": "critical",
"label": "PHP web shell", "key": "php_eval", "sha256": "abab...", "snippet": "@eval($_POST['cmd'])"}]
}
}
The result fields are those of POST /threats/scan or
POST /backups.
Returns: {"ok": true, "status": "done", "new_alerts": 1}
Errors: 400 only a machine's agent key reports a job; 404 no job j-f7d2dc41 for this machine;
409 job j-f7d2dc41 is done, not running.
Working offline
POST /fleet/offline-report
Agent. A machine that could not reach the panel keeps protecting itself with local rules. When it
is back it sends the logs it held through POST /events as usual, and this
report of what it decided alone. Each local block is written to the audit trail (agent_local_block),
with a summary (agent_offline_report), and the person on duty is alerted when it blocked anything.
The blocks expire on the machine by themselves.
Body
| Field | Type | Meaning |
|---|---|---|
from, to |
string | when the panel was out of reach |
scored |
integer | how many events the agent scored alone |
actions |
list, up to 500 | each local block, below |
Each action: ip (required), category (required), reason, score, at, until, applied
(whether the block took effect) and event_ids (up to 50).
{
"from": "2026-10-27T01:00:00+00:00",
"to": "2026-10-27T01:40:00+00:00",
"scored": 312,
"actions": [{"ip": "192.0.2.10", "category": "brute_force", "reason": "9 failed SSH logins in 2 minutes",
"score": 0.92, "at": "2026-10-27T01:12:00+00:00", "until": "2026-10-27T03:12:00+00:00",
"applied": true, "event_ids": ["evt-a1", "evt-a2"]}]
}
Returns
{
"ok": true,
"from": "2026-10-27T01:00:00+00:00",
"to": "2026-10-27T01:40:00+00:00",
"scored": 312,
"local_blocks": 1,
"reported_at": "2026-10-27T02:45:12+00:00"
}
Errors: 400 only a machine's agent sends an offline report.