Extensions
SentrAI’s outside services are extensions: TypeSafe (the Jev classifier), Telegram, Slack,
Cloudflare and AbuseIPDB. Each one lives in its own folder under mvp/extensions/, and SentrAI runs
fine with none of them. These endpoints say which ones are installed, added and set up, add or remove
one while SentrAI runs, and test one with a single harmless call. Two channels are part of SentrAI
itself and always on: the webhook for any other system, and email. They are listed too, marked
built_in.
The dashboard’s Extensions page is built on these endpoints. You fill in an extension’s settings in the settings file or on the dashboard, not through the API: see Extensions in the install guide.
Every endpoint here needs the API token (see Signing in) unless it says otherwise.
Keys are never returned, only whether they are set.
Added, installed and set up
- Installed means its folder is there. Installing or uninstalling one is adding or deleting the folder, then restarting the core and the notifier.
- Added means it is on. Adding and removing take effect while SentrAI runs.
- Set up (
configured) means its settings are filled in and fit together.
Whether an installed extension is added is decided in this order:
SENTRAI_EXTENSIONSon the server, when it is set: a comma list of the extensions that are on (nonefor none). It is the final word, and add and remove answer 409.- Otherwise the last Add or Remove made on the dashboard or through this API, kept in
core/extensions.jsonin SentrAI’s data folder. - Otherwise it counts as added once its main key is saved (
on_when), so settings files from before extensions keep working.
See what is installed
GET /extensions
Every installed extension in page order, then the two built-in channels: typesafe, telegram,
slack, cloudflare, abuseipdb, webhook, email when all five are installed.
Returns, trimmed to one extension and one built-in channel:
[
{
"name": "slack",
"title": "Slack",
"kind": "Alerts",
"what": "Posts a copy of every alert to a Slack channel. Decisions stay in Telegram and the dashboard.",
"docs": "https://api.slack.com/messaging/webhooks",
"about": "A copy of every alert in a Slack channel, through an incoming webhook.",
"settings": ["SLACK_WEBHOOK_URL"],
"on_when": ["SLACK_WEBHOOK_URL"],
"parts": ["core", "notifier"],
"source": "bundled",
"requires": {"core": ["httpx"], "notifier": ["requests"]},
"built_in": false,
"added": true,
"added_by": "dashboard",
"running": true,
"configured": true,
"detail": "The notifier posts a copy of each alert there.",
"last_ok": null,
"last_error": null,
"testable": true,
"error": null,
"changed_by": "erick",
"changed_at": "2026-10-27T12:21:07+00:00"
},
{
"name": "email",
"title": "Email",
"kind": "Alerts",
"what": "Emails alerts, as a backup to the other alert channels or always.",
"settings": ["SMTP_HOST", "ALERT_EMAIL_TO"],
"docs": "",
"about": "Part of SentrAI: set an SMTP server and the recipients.",
"built_in": true,
"added": true,
"added_by": "built_in",
"running": true,
"configured": false,
"detail": "Handled by the notifier.",
"testable": false,
"last_ok": null,
"last_error": null,
"error": null,
"source": "built_in",
"on_when": [],
"parts": [],
"requires": {}
}
]
| Field | Meaning |
|---|---|
name |
its id, for the endpoints below |
title, what, about |
its name, what it does and a longer line about it, in plain words |
kind |
Classifier, Alerts, Blocking or Lookups |
settings |
the settings it reads, as environment variables or keys in the settings file |
on_when |
the settings that count it as added when nothing else decided (step 3 above) |
parts |
where it runs: core, notifier or both |
source |
bundled (shipped with SentrAI), extra (a folder in SENTRAI_EXTENSIONS_DIR) or built_in |
requires |
the Python packages each part needs |
docs |
where to get a key, or "" |
built_in |
true for the webhook and email, which cannot be added or removed |
added |
true when it is on |
added_by |
what decided added: SENTRAI_EXTENSIONS, dashboard, settings or built_in |
running |
true when its core part is running now. A notifier-only extension is never running in the core |
configured |
true when its settings are filled in; for Cloudflare, also when they fit together |
detail |
one line: what it does now, or what it still needs |
last_ok |
the last time a call to the service worked since it started, or null. The machine’s local time, written YYYY-MM-DD HH:MM:SS, without an offset |
last_error |
what went wrong the last time, or null |
testable |
true when POST /extensions/{name}/test can test it now |
error |
why it could not start (its settings do not fit, a package is missing), or null. It then stays off, and configured is false |
changed_by, changed_at |
who last added or removed it on the dashboard, and when, or null |
A Cloudflare token without a zone or an account, for example, leaves it off with:
{
"name": "cloudflare",
"added": true,
"running": false,
"configured": false,
"error": "set CLOUDFLARE_ZONE_ID (one site) or CLOUDFLARE_ACCOUNT_ID (every site)"
}
An extension that is installed but not added says so in detail:
Not added. Add it to use it; its settings are kept either way.
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/extensions
GET /integrations
The older name of GET /extensions: the same rows. It still answers, so scripts
written before extensions keep working.
Add or remove one
POST /extensions/{name}/add
Turn an installed extension on. Its core part starts at once, and the notifier picks up its alert
part within 15 seconds. Its settings are not changed: when they are blank, it is added but not set
up, and detail says what to fill in. Writes extension_added to the audit trail.
Body
| Field | Type | Required | Meaning |
|---|---|---|---|
operator |
string | yes | who added it, for the audit trail |
Returns its row, as in GET /extensions.
Errors
- 404
no extension called nope is installed: there is no such folder, ornameis a built-in channel. - 409 when
SENTRAI_EXTENSIONSis set on the server: change it there instead.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"operator": "erick"}' http://127.0.0.1:8000/extensions/slack/add
POST /extensions/{name}/remove
Turn an installed extension off. Its settings and saved files are kept, so adding it again brings it
back as it was. Blocks it put in place at an outside service (Cloudflare) are taken out there; the
block on the machine itself stays until it ends as usual. Writes extension_removed to the audit
trail.
Body as for add: operator.
Returns its row, plus cleanup:
{
"name": "cloudflare",
"added": false,
"running": false,
"detail": "Not added. Add it to use it; its settings are kept either way.",
"cleanup": {"released": 2, "left_in_place": [], "cloudflare_removed": 2}
}
cleanup field |
Meaning |
|---|---|
released |
blocks in force that it took out of its service |
left_in_place |
the ones it could not, each with action_id, target and error. Nothing retries them: remove them at the service by hand. The incident’s timeline says so too |
Anything else in cleanup is the extension’s own note on closing.
Errors as for add: 404 and 409.
POST /extensions/reload
Read the settings file again and start, stop or restart each extension to match it. The dashboard
calls this after saving settings, so a new key works without a restart. There is no body. Writes
extensions_reloaded to the audit trail when something changed.
Returns every extension row, as in GET /extensions, without the built-in
channels.
Test a connection
POST /extensions/{name}/test
Make one harmless call with the saved settings, and say whether it worked:
cloudflarereads the place SentrAI would write to (the zone’s or the account’s IP Access rules, or the IP list) and never changes it.abuseipdblooks up 1.1.1.1, a public resolver that is never reported as abusive. It counts as one lookup on your AbuseIPDB plan, unless the answer is still kept from an earlier lookup (ABUSEIPDB_CACHE_HOURS, 24 by default).slackposts one test message to the channel, which reads:shield: SentrAI test message: alerts will arrive in this channel.typesafeasks Jev to classify one ordinary page view.telegram,webhookandemailhave no test yet, and answer withokset tofalse.
Nothing is written to the audit trail. A test also updates last_ok or last_error in
GET /extensions. The answer can take a few seconds: the core waits up to 5
seconds for Cloudflare (CLOUDFLARE_TIMEOUT_S) and for Slack, and 4 for AbuseIPDB.
There is no body.
Returns ok, with detail when the test worked or error when it did not. The status is 200
either way.
{"ok": true, "detail": "Test message posted. Check the channel."}
{"ok": false, "error": "Slack is not added."}
name |
detail when it works |
error when it does not |
|---|---|---|
cloudflare |
Token works. SentrAI will block through IP Access rules on zone 023e105f4ecef8ad9ca31a8372d0c353., or through IP list sentrai_blocklist (12 items). with an IP list |
Cloudflare’s own message about the token or the zone, or the settings problem shown as error in GET /extensions |
abuseipdb |
Key works. Each new attacking address will be looked up once a day at most. |
AbuseIPDB’s own message about the key, HTTP 429 and the like, or a network error |
slack |
Test message posted. Check the channel. |
the Slack webhook URL must start with https://; Slack answered HTTP 404 and the like; a network error |
typesafe |
Jev answered: an ordinary page view is benign (p=0.97). |
why Jev is off, or Jev did not answer. |
| any of them | Slack is not added. and the like, or what it still needs when it is added but not set up |
|
telegram |
This extension has no test. |
|
webhook, email |
Any other system (webhook) has no test here yet. or Email has no test here yet. |
Errors
- 404
no extension called nope is installed.
curl -X POST -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8000/extensions/slack/test
POST /integrations/{name}/test
The older name of POST /extensions/{name}/test, answering the same way.
What each one needs
What makes each one configured, and what its detail says once it is:
name |
kind |
configured when |
detail once set up |
|---|---|---|---|
typesafe |
Classifier | TYPESAFE_API_KEY is set and the typesafe-sdk package is installed |
12 calls since start, 0 failed, 3 answered from the cache |
telegram |
Alerts | TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are both set |
The notifier runs the bot · 2 linked |
slack |
Alerts | SLACK_WEBHOOK_URL is set and starts with https:// |
The notifier posts a copy of each alert there. |
cloudflare |
Blocking | CLOUDFLARE_API_TOKEN is set, with CLOUDFLARE_ZONE_ID (one site) or CLOUDFLARE_ACCOUNT_ID (every site). With CLOUDFLARE_METHOD=list, CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_LIST_ID instead |
IP Access rules, block on zone 023e105f4ecef8ad9ca31a8372d0c353 · 1 address blocked now, or IP list on account · 0 addresses blocked now |
abuseipdb |
Lookups | ABUSEIPDB_API_KEY is set |
12 lookups since start, answers kept 24 h |
webhook |
Blocking | SENTRAI_RESPONDER_WEBHOOK_URL is set |
Sends block_ip, lock_user to hooks.example.com: the action types it hands over (SENTRAI_RESPONDER_WEBHOOK_ACTIONS) and the host it sends them to |
email |
Alerts | SMTP_HOST and ALERT_EMAIL_TO are both set |
Handled by the notifier. |
CLOUDFLARE_METHODisaccess_rule(the default) orlist.CLOUDFLARE_MODEisblock(the default),managed_challenge,challengeorjs_challenge. Any other value keeps Cloudflare off, with the reason inerror.- The address count in Cloudflare’s
detailis the blocks SentrAI put there itself and has not removed yet. Protected and private addresses are never sent. - Telegram, Slack and email alerts are sent by the notifier, not by the core. The core only reports whether they are set, and tests Slack.
- Telegram’s own endpoints (linking a person’s chat, see Alerts) answer 404 while Telegram is not added.
- What the webhook sends for each block and its removal, and how the receiver checks that it came from SentrAI, is on What SentrAI sends out.