Base URL: http://<host>:65000/api
All request/response bodies are JSON (Content-Type: application/json) unless noted otherwise.
Network access: The API is designed for the embedded Vue frontend only — not for external consumption. The server binds to 127.0.0.1 (localhost) by default, rejecting non-local connections. To allow remote access (e.g., inside a Docker container), set BEKCI_HOST=0.0.0.0 or server.host: "0.0.0.0" in config.yaml.
Compression: All API responses are gzip-compressed when the client sends Accept-Encoding: gzip. Handled by gzipMiddleware (stdlib compress/gzip), wrapping the outermost layer of the router chain.
Two authentication modes co-exist:
- Cookie JWT (default for the embedded Vue frontend and for
/api/*). OnPOST /api/login, the server sets atokencookie (HttpOnly, Secure, SameSite=Strict). All authenticated/api/*endpoints read the JWT from this cookie automatically — noAuthorizationheader needed. - Bearer API token (for
/api/v1/*machine endpoints). Admin-issued bearer tokens in theAuthorization: Bearer bk_…header. Tokens are managed in Settings → Users → API Access. Plaintext is shown once at creation, never retrievable afterward. See Machine API (v1) below.
Roles (hierarchical for RBAC checks):
admin-- full accessoperator-- monitoring management (targets, checks, recipients, run checks, audit log, user list)viewer-- read-only dashboards and monitoring data
Error format (all endpoints):
{ "error": "error message" }Rate limiting: Login endpoint is rate-limited per IP and per username (two independent limiters) -- 5 attempts per 5-minute window, 15-minute lockout after threshold. Blocked if either limiter triggers.
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/login |
public | Authenticate and get JWT |
| POST | /api/logout |
any | Invalidate current session |
| GET | /api/me |
any | Get current user profile |
| PUT | /api/me |
any | Update own profile |
| PUT | /api/me/password |
any | Change own password |
Rate limited: 5 attempts/5min per IP + per username (dual limiters), 15min lockout.
Request:
{
"username": "string",
"password": "string"
}Response (200):
Sets Set-Cookie: token=<jwt>; Path=/; MaxAge=<session_timeout>; HttpOnly; Secure; SameSite=Strict
{
"user": {
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}| Error | Code |
|---|---|
| Missing fields | 400 |
| Invalid credentials | 401 |
| Rate limited | 429 |
Response (200):
{ "message": "logged out" }Response (200):
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"phone": "+1234567890",
"role": "admin",
"status": "active"
}Request: (all fields optional, empty = keep current)
{
"email": "new@example.com",
"phone": "+1234567890"
}Response (200):
{ "message": "profile updated" }Invalidates all other sessions on success.
Request:
{
"current_password": "string",
"new_password": "string (min 15 chars)"
}Response (200):
{ "message": "password changed" }| Error | Code |
|---|---|
| Password too short (<15) | 400 |
| Wrong current password | 401 |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/users |
operator+ | List all users |
| POST | /api/users |
admin | Create user |
| GET | /api/users/{id} |
admin | Get user by ID |
| PUT | /api/users/{id} |
admin | Update user |
| PUT | /api/users/{id}/suspend |
admin | Suspend/activate user |
| PUT | /api/users/{id}/password |
admin | Reset user password |
Response (200): Array of User objects, sorted alphabetically by username (case-insensitive).
[
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"phone": "",
"role": "admin",
"status": "active",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
]Request:
{
"username": "string (required)",
"email": "string",
"password": "string (required, min 15 chars)",
"role": "admin | operator | viewer (required)"
}Response (201):
{
"id": "uuid",
"username": "newuser",
"email": "user@example.com",
"role": "operator",
"status": "active"
}| Error | Code |
|---|---|
| Missing required fields | 400 |
| Password too short (<15) | 400 |
| Invalid role | 400 |
| Username exists | 409 |
Response (200):
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"status": "active",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}Request: (all fields optional, empty = keep current)
{
"email": "string",
"phone": "string",
"role": "admin | operator | viewer"
}Response (200):
{ "message": "user updated" }| Error | Code |
|---|---|
| Invalid role | 400 |
| User not found | 404 |
| Demoting last active admin | 409 |
Suspending a user kills all their active sessions.
Request:
{
"suspended": true
}Response (200):
{ "message": "user status updated" }| Error | Code |
|---|---|
| User not found | 404 |
| Suspending last active admin | 409 |
Kills all sessions for the user, forcing re-login.
Request:
{
"password": "string (min 15 chars)"
}Response (200):
{ "message": "password reset" }| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/targets |
any | List target summaries |
| POST | /api/targets |
operator+ | Create target with conditions |
| GET | /api/targets/{id} |
any | Get target detail |
| PUT | /api/targets/{id} |
operator+ | Update target with conditions |
| DELETE | /api/targets/{id} |
operator+ | Delete target |
| POST | /api/targets/{id}/pause |
operator+ | Pause target (stops checks) |
| POST | /api/targets/{id}/unpause |
operator+ | Unpause target (resumes checks) |
Returns summary list (no full conditions, includes condition count and state).
Response (200):
[
{
"id": "uuid",
"name": "Web Server",
"host": "192.168.1.1",
"description": "Main web server",
"enabled": true,
"preferred_check_type": "http",
"operator": "AND",
"category": "Network",
"rule_id": "uuid",
"notes": "Main web server notes",
"contacts": "ops@example.com",
"project": "DIAS",
"location": "DC-1",
"tags": ["DEVOPS", "IT", "P1"],
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z",
"condition_count": 2, // number of checks for this target
"state": {
"rule_id": "uuid",
"current_state": "healthy",
"last_change": "2026-01-15T10:00:00Z",
"last_evaluated": "2026-01-15T10:05:00Z"
}
}
]Notes: state is null for targets with no rule (no conditions). state.last_change and state.last_evaluated are null until the first evaluation. notes, contacts, project, location are null when not set. tags is an (often empty) array of uppercase free-form labels from tag_options where grp='tag', hydrated via target_tags.
Creates target, checks, rule, and rule conditions in one transaction. Creator is auto-added as alert recipient.
Request:
{
"name": "string (required)",
"host": "string (required)",
"description": "string",
"enabled": true,
"operator": "AND (kept for backward compat, used as fallback for empty group_operator)",
"category": "string (must exist in tag_options where grp='category', default: Other)",
"preferred_check_type": "string (optional, must match a condition's check_type; defaults to 'ping'; validated against conditions, falls back to first condition's type if no ping)",
"notes": "string (optional)",
"contacts": "string (optional)",
"project": "string (optional, must exist in tag_options)",
"location": "string (optional, must exist in tag_options)",
"tags": ["string (optional, each must exist in tag_options where grp='tag'; uppercased on the server before lookup; unknown → 400)"],
"conditions": [
{
"check_type": "http | tcp | ping | dns | page_hash | tls_cert | snmp_v2c | snmp_v3 (required)",
"check_name": "string (required)",
"config": "JSON string (default: {})",
"interval_s": 300,
"field": "string (default: status)",
"comparator": "string (default: eq)",
"value": "string (default: down)",
"fail_count": 1,
"fail_window": 0,
"condition_group": 0,
"group_operator": "AND | OR (default: AND)"
}
]
}Condition groups: Conditions are grouped by condition_group (integer). Within a group, conditions are combined using group_operator (AND/OR). Across groups, the logic is always OR — any group triggering means the target is unhealthy. Example: (TCP AND HTTP) OR PING = group 0 with AND (TCP, HTTP), group 1 with AND (PING).
If group_operator is omitted, it defaults to the top-level operator value for backward compatibility.
Failure evaluation modes:
fail_window > 0: Counts consecutive matching results from newest (streak mode). Must reachfail_countconsecutive failures to trigger the condition.fail_window = 0: Single result check ("Once" mode). Triggers on a single matching result;fail_countis ignored.
Normalization: When fail_count > 1 and fail_window = 0, API auto-sets fail_window = interval_s (clamped min=interval_s, max=1800)
Response (201): Full TargetDetail object (see GET /api/targets/{id}).
| Error | Code |
|---|---|
| Missing name/host | 400 |
| No conditions provided | 400 |
| Invalid operator | 400 |
| Invalid category | 400 |
| Invalid check_type | 400 |
| Missing check_name | 400 |
| Invalid group_operator | 400 |
| Invalid project tag | 400 |
| Invalid location tag | 400 |
Unknown tag (tags[]) |
400 |
| Duplicate target name | 409 |
Returns full target detail with conditions, state, and recipient IDs.
Response (200):
{
"id": "uuid",
"name": "Web Server",
"host": "192.168.1.1",
"description": "Main web server",
"enabled": true,
"preferred_check_type": "http",
"operator": "AND",
"category": "Network",
"rule_id": "uuid",
"notes": "Main web server",
"contacts": "ops@example.com",
"project": "DIAS",
"location": "DC-1",
"tags": ["DEVOPS", "IT", "P1"],
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z",
"conditions": [
{
"check_id": "uuid",
"check_type": "http",
"check_name": "HTTP Check",
"config": "{\"url\":\"http://192.168.1.1\"}",
"interval_s": 300,
"field": "status",
"comparator": "eq",
"value": "down",
"fail_count": 3,
"fail_window": 600,
"condition_group": 0,
"group_operator": "AND"
}
],
"state": {
"rule_id": "uuid",
"current_state": "healthy",
"last_change": "2026-01-15T10:00:00Z",
"last_evaluated": "2026-01-15T10:05:00Z"
},
"recipient_ids": ["uuid1", "uuid2"]
}Smart-diffs conditions: existing checks with check_id are updated, new conditions (no check_id) create new checks, missing checks are deleted.
Note: A check's type is immutable after creation. To change a check's type (e.g. http to ping), delete the condition and add a new one with the desired type. The check_type field in update payloads for existing checks is ignored.
Request: Same structure as POST /api/targets.
Conditions can include check_id to update existing checks:
{
"conditions": [
{
"check_id": "existing-uuid",
"check_type": "http",
"check_name": "Updated Name",
"config": "{}",
"interval_s": 60
},
{
"check_type": "ping",
"check_name": "New Check"
}
]
}Response (200): Full TargetDetail object.
| Error | Code |
|---|---|
| Missing name/host | 400 |
| No conditions provided | 400 |
| Invalid operator/category/check_type | 400 |
| Target not found | 404 |
| Duplicate target name | 409 |
Deletes target, all linked checks, rule, and conditions. Triggers scheduler reload.
Response (200):
{ "status": "ok" }| Error | Code |
|---|---|
| Target not found | 404 |
Pauses the target and stops all its checks from running. Records pause event in history.
Response (200):
{ "status": "paused" }| Error | Code |
|---|---|
| Target not found | 404 |
| Already paused | 400 |
Unpauses the target and immediately triggers RunNow on all its checks.
Response (200):
{ "status": "unpaused" }| Error | Code |
|---|---|
| Target not found | 404 |
| Not paused | 400 |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/tags?group=project|location|category |
socAuth | List tag values for a group (public when soc_public=true) |
| POST | /api/tags |
admin | Create a tag value |
| PUT | /api/tags/{id} |
admin | Rename a tag value (category: cascades to targets + SLA key) |
| DELETE | /api/tags/{id} |
admin | Delete a tag value (project/location: cascade-clears; category: blocked if in use) |
Query params:
| Param | Required | Values |
|---|---|---|
group |
Yes | project, location, category, or tag |
Response (200):
[
{ "id": 1, "group": "project", "value": "DIAS" },
{ "id": 2, "group": "project", "value": "SCADA" }
]| Error | Code |
|---|---|
| Missing/invalid group | 400 |
Request:
{
"group": "project | location | category | tag",
"value": "string (required)"
}Response (201):
{ "id": 3, "group": "location", "value": "DC-1" }Notes:
- For
group: "tag", the server uppercases + trimsvaluebefore insert (e.g.p1→P1). The response shows the canonical form. (group, value)must be unique.
| Error | Code |
|---|---|
| Missing/invalid group | 400 |
| Empty value | 400 |
| Duplicate value in group | 409 |
Renames a tag option. For categories: cascades to targets.category and renames the SLA settings key. "Other" category cannot be renamed. For tag group: value is uppercased before save; no cascade needed (targets reference tags by id via target_tags).
Request:
{ "value": "New Name" }Response (200):
{ "status": "ok" }| Error | Code |
|---|---|
| Tag not found | 404 |
| Empty value | 400 |
| Duplicate name | 409 |
| Cannot rename "Other" | 400 |
For project/location: deletes the tag and sets the field to NULL on all targets using it.
For category: blocked if any targets use this category. "Other" cannot be deleted.
For tag: deletes the catalog row; DDL-level ON DELETE CASCADE on target_tags automatically removes the tag from every target that used it.
Response (200):
{ "status": "ok" }Error (409 — category in use):
{
"error": "category has 3 assigned targets",
"targets": ["Web Server", "Firewall-01", "DB Server"]
}| Error | Code |
|---|---|
| Tag not found | 404 |
| Cannot delete "Other" | 400 |
| Category has assigned targets | 409 |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/targets/{id}/recipients |
operator+ | List alert recipients for target |
| PUT | /api/targets/{id}/recipients |
operator+ | Set alert recipients for target |
Response (200): Array of User objects (same as GET /api/users list format).
[
{
"id": "uuid",
"username": "admin",
"email": "admin@example.com",
"phone": "",
"role": "admin",
"status": "active",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
]Replaces all recipients for the target.
Request:
{
"user_ids": ["uuid1", "uuid2"]
}Response (200):
{ "message": "recipients updated" }| Error | Code |
|---|---|
| Target not found | 404 |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/targets/{id}/checks |
any | List checks for a target |
| POST | /api/checks/{id}/run |
operator+ | Trigger immediate check execution |
| GET | /api/checks/{id}/results |
any | Get recent results (last 24h) |
Response (200):
[
{
"id": "uuid",
"target_id": "uuid",
"type": "http",
"name": "HTTP Check",
"config": "{\"url\":\"http://example.com\"}",
"interval_s": 300,
"enabled": true,
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-15T10:00:00Z"
}
]Queues the check for immediate execution via the scheduler.
Response (200):
{ "status": "queued" }| Error | Code |
|---|---|
| Check not found | 404 |
Returns raw check results from the last 24 hours.
Response (200):
[
{
"id": 1,
"check_id": "uuid",
"status": "up",
"response_ms": 45,
"message": "HTTP 200 OK",
"metrics": "{}",
"checked_at": "2026-01-15T10:00:00Z"
}
]The config field in target conditions is a JSON string with check-type-specific fields. All fields are optional — sensible defaults are applied. Config is parsed at runtime using helpers that return defaults for missing keys.
| Field | Type | Default | Description |
|---|---|---|---|
scheme |
string | "https" |
http or https |
port |
int | 0 |
Port (0 = scheme default: 80/443) |
endpoint |
string | "/" |
URL path |
expect_status |
int | 200 |
Expected HTTP status code |
skip_tls_verify |
bool | false |
Skip TLS cert verification |
timeout_s |
int | 10 |
Request timeout (seconds) |
{ "scheme": "https", "port": 8443, "endpoint": "/health", "expect_status": 200 }Metrics: status_code, url. Status "down" if response code != expect_status.
| Field | Type | Default | Description |
|---|---|---|---|
port |
int | 80 |
Destination port |
timeout_s |
int | 5 |
Connection timeout (seconds) |
{ "port": 443, "timeout_s": 10 }Metrics: addr. Simple connect-then-close test.
| Field | Type | Default | Description |
|---|---|---|---|
count |
int | 3 |
ICMP packets to send |
timeout_s |
int | 5 |
Timeout for entire sequence (seconds) |
{ "count": 5, "timeout_s": 10 }Metrics: packet_loss, avg_rtt_ms, packets_sent, packets_recv. Status "down" only on 100% packet loss. Requires NET_RAW capability or runs in unprivileged UDP mode.
| Field | Type | Default | Description |
|---|---|---|---|
query |
string | target host | Domain to resolve |
record_type |
string | "A" |
A, AAAA, MX, or CNAME |
expect_value |
string | "" |
Expected value (empty = any resolution is "up") |
nameserver |
string | "" |
Custom nameserver (empty = system resolver) |
timeout_s |
int | 5 |
Query timeout (seconds) |
{ "query": "www.example.com", "record_type": "A", "expect_value": "93.184.216.34", "nameserver": "8.8.8.8" }Metrics: query, record_type, resolved (array). Trailing dots stripped for CNAME comparison.
| Field | Type | Default | Description |
|---|---|---|---|
scheme |
string | "https" |
http or https |
port |
int | 0 |
Port (0 = scheme default) |
endpoint |
string | "/" |
URL path to fetch |
baseline_hash |
string | "" |
SHA256 of expected body (empty = capture mode) |
skip_tls_verify |
bool | false |
Skip TLS cert verification |
timeout_s |
int | 10 |
Request timeout (seconds) |
{ "endpoint": "/index.html", "baseline_hash": "e3b0c44298fc1c14..." }Metrics: hash, baseline_hash, url, baseline_captured. On first run (empty baseline_hash), returns "up" with baseline_captured: true and the computed hash. Subsequent runs compare against baseline — "down" on mismatch. Body limited to 2MB.
| Field | Type | Default | Description |
|---|---|---|---|
port |
int | 443 |
TLS port |
warn_days |
int | 30 |
Days before expiry to flag "down" |
timeout_s |
int | 10 |
Connection timeout (seconds) |
{ "port": 443, "warn_days": 14 }Metrics: days_left, issuer, subject, not_after, not_before. Uses SNI for hostname matching. Connects with InsecureSkipVerify to inspect even invalid certs. Status "down" if cert expires within warn_days or already expired.
| Field | Type | Default | Description |
|---|---|---|---|
port |
int | 161 |
SNMP UDP port |
timeout_s |
int | 5 |
Request timeout (seconds) |
{ "port": 161, "timeout_s": 5 }Credentials: uses snmp_v2c_community from global settings (not per-check config).
Metrics: sys_descr, sys_uptime_s, sys_contact, sys_name, cpu_avg_pct (best-effort), memory_total_kb (best-effort). Status "up" if SNMP responds, "down" on timeout or auth failure.
| Field | Type | Default | Description |
|---|---|---|---|
port |
int | 161 |
SNMP UDP port |
timeout_s |
int | 5 |
Request timeout (seconds) |
{ "port": 161, "timeout_s": 10 }Credentials: uses snmp_v3_* keys from global settings (username, security_level, auth_protocol, auth_passphrase, privacy_protocol, privacy_passphrase). Not per-check config.
Metrics: same as snmp_v2c (sys_descr, sys_uptime_s, sys_contact, sys_name, cpu_avg_pct, memory_total_kb). Status "up" if SNMP responds, "down" on timeout or auth failure.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/dashboard/status |
any | Get all targets with check status |
| GET | /api/dashboard/history/{checkId} |
any | Get check history (4h or 90d) |
Returns enabled targets with their checks, last status, response time, and 90-day uptime. Disabled targets (enabled=false) are filtered out. Paused targets are included with pause metadata. Data sourced from check_state (current status) + check_daily_rollups (90d uptime) — does not query check_results.
Response (200):
[
{
"id": "uuid",
"name": "Web Server",
"host": "192.168.1.1",
"preferred_check_type": "http",
"state": "healthy",
"paused": false,
"paused_at": null,
"category": "Network",
"sla_status": "healthy",
"sla_target": 99.9,
"checks": [
{
"id": "uuid",
"name": "HTTP Check",
"type": "http",
"enabled": true,
"interval_s": 300,
"last_status": "up",
"last_message": "HTTP 200 OK",
"response_ms": 45,
"uptime_90d_pct": 99.95
}
]
}
]Notes:
- Disabled targets (
enabled=false) are excluded from the response. - Paused targets have
state: "paused",paused: true, andpaused_atset to an ISO 8601 timestamp. sla_statusis""(empty) andsla_targetis0when the category's SLA threshold is 0 (disabled) or not configured.sla_statusis also""when no check results exist yet for the preferred check.
Query params:
| Param | Values | Description |
|---|---|---|
range |
4h |
Raw results for last 4 hours (from check_results) |
range |
90d (or empty) |
Daily uptime percentages for last 90 days (from check_daily_rollups) |
Response (200) -- range=4h: Slim format (3 fields only, for bar rendering):
[
{
"status": "up",
"response_ms": 45,
"checked_at": "2026-01-15T10:00:00Z"
}
]Note: Unlike GET /api/checks/{id}/results (which returns full CheckResult with 7 fields), this endpoint returns only the fields needed for history bar rendering.
Response (200) -- range=90d (default):
[
{
"date": "2026-01-15",
"uptime_pct": 99.5,
"total_checks": 288
}
]| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/sla/history |
any | 90-day daily uptime per category |
Returns all categories with per-target daily uptime arrays for the preferred check. Disabled targets (enabled=false) are filtered out. Categories are loaded dynamically from tag_options (grp='category'), sorted alphabetically with "Other" always last.
Response (200):
{
"categories": [
{
"name": "Network",
"sla_threshold": 99.9,
"targets": [
{
"id": "uuid",
"name": "Router-1",
"daily_uptime": [
{ "date": "2026-01-20", "uptime_pct": 100.0 },
{ "date": "2026-01-21", "uptime_pct": 99.65 }
]
}
]
}
],
"pause_stats": {
"count": 2,
"affected_hosts": 1
}
}Notes:
sla_thresholdis0when the category has no SLA configured (disabled)targetsis[](empty array) for categories with no targetsdaily_uptimecontains only days with data incheck_daily_rollups(frontend pads to 90 days)- Uses the target's preferred check type; falls back to first check if no match
- Data sourced from
check_daily_rollups(pre-aggregated) — does not querycheck_results
SOC endpoints return the same data as Dashboard but with conditional auth: if the soc_public setting is "true", these endpoints are publicly accessible without authentication. Otherwise, standard cookie-based auth is required.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/soc/status |
conditional | Same as dashboard/status |
| GET | /api/soc/history/{checkId} |
conditional | Same as dashboard/history |
Response formats are identical to their Dashboard counterparts.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/alerts |
any | List alert history (paginated) |
| POST | /api/settings/test-email |
admin | Send test email to current user |
| POST | /api/settings/test-signal |
admin | Send test Signal message to a phone number |
| POST | /api/settings/test-webhook |
admin | Send test webhook to configured endpoint |
| GET | /api/settings/webhook-status |
admin | Get webhook delivery status |
Query params:
| Param | Default | Constraints |
|---|---|---|
page |
1 | >= 1 |
limit |
50 | 1-100 |
Response (200):
{
"entries": [
{
"id": 1,
"rule_id": "uuid",
"target_id": "uuid",
"target_name": "Web Server",
"recipient_id": "uuid",
"recipient_name": "admin",
"alert_type": "firing",
"message": "Target Web Server is unhealthy",
"sent_at": "2026-01-15T10:00:00Z"
}
],
"total": 42
}recipient_name is the username for per-user channels (email, Signal). For webhook entries — which have no per-user recipient — recipient_id is empty and recipient_name is derived from the message prefix: Webhook, Signal (global), or (channel) as a fallback.
alert_type values: "firing", "recovery", "re-alert". Cooldown (alert_cooldown_s) applies to all alert types including recovery — prevents unlimited recovery alerts from flapping targets. Recovery alerts include downtime duration (down since, recovered at, total duration).
Sends a test email. Requires the alerter service to be configured.
Request: Optional body to override recipient:
{ "email": "override@example.com" }If no body or empty email, sends to the authenticated user's email address.
Response (200):
{ "message": "test email sent to admin@example.com" }| Error | Code |
|---|---|
| No email on account | 400 |
| User not found | 404 |
| Email send failed | 500 |
| Alerter not initialized | 503 |
Sends a test Signal message to the specified phone number. Requires Signal gateway settings to be configured.
Request:
{
"phone": "+1234567890"
}Response (200):
{ "message": "test signal sent to +1234567890" }| Error | Code |
|---|---|
| No phone provided | 400 |
| Signal not configured | 500 |
| Signal send failed | 500 |
| Alerter not initialized | 503 |
Sends a test webhook payload to the configured webhook URL. Uses the configured auth type (none, Bearer, or Basic). Requires webhook to be enabled with a URL configured.
Request: No body required.
Response (200):
{ "message": "test webhook sent successfully" }| Error | Code |
|---|---|
| Webhook not configured | 500 |
| Webhook send failed | 500 |
| Alerter not initialized | 503 |
Returns the last webhook delivery error and success timestamps.
Response (200):
{
"last_error": "2026-03-12T14:30:00Z — webhook error 500: server error",
"last_success": "2026-03-12T14:00:00Z"
}Both fields are empty strings when no webhook has been sent yet.
Bekci POSTs the following JSON to webhook_url on firing, recovery, re-alert, and test events. Content-Type: application/json. Auth is the configured Bearer/Basic header, TLS verification is on unless webhook_skip_tls=true, timeout is webhook_timeout_s (default 10s).
{
"event": "firing | recovery | re-alert | test",
"target": "Web Server",
"target_address": "192.168.1.10",
"category": "Network",
"tags": ["DEVOPS", "IT", "P1"],
"message": "Target Web Server is unhealthy",
"failing_checks": [
{ "type": "http", "detail": "status 500" }
],
"timestamp": "2026-04-24T09:13:00Z",
"down_since": "2026-04-24T09:00:00Z",
"duration": "13m"
}Notes:
tagsis an array of uppercase free-form labels attached to the target (fromtag_optionswheregrp='tag'). Always present;[]when the target has no tags. Thetestevent sends["TEST"]as a sentinel.failing_checksis[]forrecoveryandtest; populated forfiringandre-alertwith every check whose last result isn'tup.down_since+durationare only included onrecovery.- Non-2xx response is treated as failure (recorded in
webhook_last_error, audit-logged); no retries.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/settings |
admin | Get all settings |
| PUT | /api/settings |
admin | Update settings |
Returns all settings as key-value map. Admin only (viewers/operators get 403). Sensitive values (e.g. resend_api_key, snmp_v2c_community, snmp_v3_auth_passphrase, snmp_v3_privacy_passphrase) are masked. Only keys with existing DB rows are returned; unseeded keys absent.
Response (200):
{
"session_timeout_hours": "24",
"history_days": "3",
"audit_retention_days": "91",
"soc_public": "false",
"alert_method": "email",
"email_provider": "resend",
"resend_api_key": "re_1234****",
"alert_from_email": "alerts@example.com",
"alert_cooldown_s": "300",
"alert_realert_s": "3600",
"smtp_host": "smtp.example.com",
"smtp_port": "587",
"smtp_username": "user@example.com",
"smtp_password": "••••••••",
"signal_api_url": "https://signal-api.example.com/v2/send",
"signal_number": "+1234567890",
"signal_username": "user",
"signal_password": "••••••••",
"signal_skip_tls": "false",
"sla_network": "99.9",
"sla_security": "99.9",
"sla_physical_security": "99.9",
"sla_key_services": "99.9",
"sla_other": "99.9",
"snmp_v2c_community": "public",
"snmp_v3_username": "",
"snmp_v3_security_level": "authPriv",
"snmp_v3_auth_protocol": "SHA",
"snmp_v3_auth_passphrase": "••••••••",
"snmp_v3_privacy_protocol": "AES",
"snmp_v3_privacy_passphrase": "••••••••",
"backup_max_copies": "5"
}Update one or more settings. Only known keys are accepted. Sending masked values ("••••••••" or "****") for resend_api_key, signal_password, smtp_password, webhook_bearer_token, webhook_basic_password, snmp_v3_auth_passphrase, or snmp_v3_privacy_passphrase is silently ignored (preserves existing value).
Request:
{
"session_timeout_hours": "24",
"soc_public": "true"
}Known settings:
| Key | Type | Validation |
|---|---|---|
session_timeout_hours |
positive integer | >= 1 |
history_days |
positive integer | >= 1 (raw result retention; code defaults to 3 days, this setting overrides if higher) |
audit_retention_days |
positive integer | >= 1 |
soc_public |
boolean string | "true" or "false" |
api_rate_limit_per_min |
positive integer | 1 – 10000. Per-token flat rate limit on /api/v1/*. Change takes effect within 30s (invalidation is immediate on save — 30s is the worst case between existing and updated windows). |
alert_method |
string | "", "email", "signal", or "email+signal" |
email_provider |
string | "", "resend", or "ms365" |
resend_api_key |
string | any string (empty to clear; masked in GET: first 7 chars + ****) |
alert_from_email |
string | any string |
smtp_host |
string | SMTP server hostname |
smtp_port |
string | SMTP server port (e.g. "587") |
smtp_username |
string | SMTP auth username |
smtp_password |
string | SMTP auth password (masked in GET as "••••••••") |
alert_cooldown_s |
non-negative integer | >= 0 |
alert_realert_s |
non-negative integer | >= 0 |
signal_api_url |
string | any string (full gateway URL) |
signal_number |
string | any string (sender phone number) |
signal_username |
string | any string |
signal_password |
string | any string (masked in GET as "••••••••") |
signal_skip_tls |
boolean string | "true" or "false" |
webhook_enabled |
boolean string | "true" or "false" |
webhook_url |
string | must start with http:// or https:// (empty to clear) |
webhook_auth_type |
string | "" (none), "bearer", or "basic" |
webhook_bearer_token |
string | any string (masked in GET as "••••••••"), used when auth_type=bearer |
webhook_basic_username |
string | any string, used when auth_type=basic |
webhook_basic_password |
string | any string (masked in GET as "••••••••"), used when auth_type=basic |
webhook_skip_tls |
boolean string | "true" or "false" |
webhook_last_error |
string | auto-set by system; writable via PUT |
webhook_last_success |
string | auto-set by system; writable via PUT |
snmp_v2c_community |
string | SNMP v2c community string |
snmp_v3_username |
string | SNMP v3 USM username |
snmp_v3_security_level |
string | "noAuthNoPriv", "authNoPriv", or "authPriv" |
snmp_v3_auth_protocol |
string | "MD5" or "SHA" |
snmp_v3_auth_passphrase |
string | SNMP v3 auth passphrase (masked in GET as "••••••••") |
snmp_v3_privacy_protocol |
string | "DES" or "AES" |
snmp_v3_privacy_passphrase |
string | SNMP v3 privacy passphrase (masked in GET as "••••••••") |
sla_network |
float string | 0–100 (0 = disabled) |
sla_security |
float string | 0–100 (0 = disabled) |
sla_physical_security |
float string | 0–100 (0 = disabled) |
sla_key_services |
float string | 0–100 (0 = disabled) |
sla_other |
float string | 0–100 (0 = disabled) |
backup_max_copies |
positive integer | >= 1 (max saved backups on server) |
webhook_timeout_s |
positive integer string | HTTP timeout for outbound webhook POST. Default 10s; UI accepts 1–120. Values <= 0 fall back to 10. |
auto_backup_schedule |
string | "", "off", "weekly", "10days", or "monthly". Controls the hourly goroutine's scheduled backup cadence. |
auto_backup_time |
string | HH:MM (24-hour, UTC) — the hour/minute on which the scheduled backup fires. |
system_alert_admins |
boolean string | "true" or "false". When true, every active admin receives system-level alerts (scheduler stale, unclean restart). Defaults to true on first boot. |
system_alert_users |
string | Comma-separated list of user IDs (UUIDs) that additionally receive system-level alerts, independent of role. Empty string = no extra recipients. |
Response (200):
{ "message": "settings updated" }| Error | Code |
|---|---|
| Unknown setting key | 400 |
| Invalid value for type | 400 |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/audit-log |
operator+ | List audit log entries (paginated) |
Query params:
| Param | Default | Constraints |
|---|---|---|
page |
1 | >= 1 |
limit |
50 | 1-100 |
q |
(empty) | Search across username, action, detail, ip_address, resource_type. Max 100 chars. |
Response (200):
{
"entries": [
{
"id": 1,
"user_id": "uuid",
"username": "admin",
"action": "login",
"resource_type": "session",
"resource_id": "",
"detail": "",
"ip_address": "192.168.1.100",
"status": "success",
"created_at": "2026-01-15T10:00:00Z"
}
],
"total": 150,
"page": 1,
"limit": 50
}Audit actions: login, login_failed, logout, create_user, update_user, suspend_user, activate_user, reset_password, change_password, change_password_failed, update_profile, create_target, update_target, delete_target, pause_target, unpause_target, set_alert_recipients, create_tag, delete_tag, rename_tag, update_settings, restore_backup, export_backup, export_full_backup, save_full_backup, download_saved_backup, delete_saved_backup, run_check, test_email, test_signal, test_webhook, webhook_dispatch, api_token_create, api_token_revoke, api_rate_limit_exceeded, auto_backup.
webhook_dispatch is logged by the system (user=system) for all webhook events. Detail contains the event type: test, firing, recovery, re-alert, or {type} — error: {message} on failure.
Status values: success, failure. All mutating actions log both success and failure (with detail). Login and change_password also log dedicated failure actions (login_failed, change_password_failed).
IP address: All audit entries include the client's source IP. Behind a reverse proxy, X-Real-IP header is used when RemoteAddr is loopback.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/backup |
admin | Download config-only backup (JSON) |
| POST | /api/backup/restore |
admin | Restore from config backup file |
| POST | /api/backup/full |
admin | Download full database backup (tar.gz) |
| POST | /api/backup/full/save |
admin | Save full backup to server |
| GET | /api/backup/full/list |
admin | List saved backups on server |
| GET | /api/backup/full/saved/{filename} |
admin | Download a saved backup |
| DELETE | /api/backup/full/saved/{filename} |
admin | Delete a saved backup |
| GET | /api/backup/generate-passphrase |
admin | Generate a random 4-word passphrase |
Returns a JSON file download containing all config data (users with hashed passwords, targets, checks, rules, settings, recipients). Does NOT include historical data (check_results, check_state, check_daily_rollups, audit_logs, alert_history).
Response headers:
Content-Type: application/json
Content-Disposition: attachment; filename="bekci-config-20260115-100000.json"
Response body: BackupData JSON structure:
{
"version": 1,
"schema_version": 22,
"created_at": "2026-01-15T10:00:00Z",
"app_version": "1.2.0",
"users": [],
"settings": {},
"rules": [],
"targets": [],
"checks": [],
"rule_conditions": [],
"rule_states": [],
"recipients": [],
"pause_history": []
}Accepts either multipart form upload (field name: file) or raw JSON body. Destructive -- wipes all config tables and replaces with backup data. Max body: 2MB.
Request (multipart):
Content-Type: multipart/form-data
Field: file = <backup.json>
Request (raw JSON): Same BackupData structure as returned by GET /api/backup.
Validation:
versionmust be1schema_versionmust not exceed current server schema- Must contain at least one active admin user
Response (200):
{ "message": "restore successful" }| Error | Code |
|---|---|
| Invalid JSON / form data | 400 |
| Unsupported backup version | 400 |
| Schema version too new | 400 |
| No active admin in backup | 400 |
Downloads a complete database backup as a tar.gz archive containing the SQLite database file and config.yaml. Optionally encrypts the archive with AES-256-GCM (Argon2id KDF).
Note: Downloads also trigger an automatic server-side backup save (subject to
backup_max_copieslimit).
Request:
{
"encrypt": true,
"passphrase": "string (required if encrypt=true, min 8 chars)"
}Both fields are optional. Omit or send {} for an unencrypted backup.
Response headers:
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="bekci-full-20260115-100000.tar.gz"
Content-Length: <size>
File extension: .tar.gz (plain) or .tar.gz.enc (encrypted).
Archive contents (tar.gz):
bekci.db— full SQLite database snapshot (via online backup API)config.yaml— server config file (if available; omitted in env-only setups)
Encryption format: salt (16B) || nonce (12B) || AES-256-GCM ciphertext+tag. Key derived via Argon2id (time=3, mem=64MB, threads=4).
| Error | Code |
|---|---|
| Passphrase too short (<8 chars) | 400 |
| Backup/encryption failure | 500 |
Restore: Full backups cannot be restored via the web UI. Use the CLI: bekci restore-full <archive-path>. See reference/full_backup.md.
Same as POST /api/backup/full but saves the archive to the server-side backup directory instead of streaming to browser. Same JSON body (encrypt, passphrase).
Response (200):
{ "message": "backup saved", "filename": "bekci-full-20260307-015116.tar.gz", "sha256": "82d676c68f5e..." }| Error | Code |
|---|---|
| Passphrase too short (<8 chars) | 400 |
| Backup/encryption/write failure | 500 |
Lists saved backups on the server with metadata (SHA256 hash, size, date, encrypted flag).
Response (200):
[
{ "filename": "bekci-full-20260307-015116.tar.gz", "sha256": "82d676c68f5e...", "size": 535424, "created_at": "2026-03-07T01:51:16Z", "encrypted": false }
]Download a previously saved backup file. Filename must match ^bekci-full-\d{8}-\d{6}\.tar\.gz(\.enc)?$.
Response: Binary stream with Content-Disposition: attachment.
| Error | Code |
|---|---|
| Invalid filename | 400 |
| Backup not found | 404 |
Delete a saved backup file. Same filename validation.
Response (200):
{ "message": "deleted" }| Error | Code |
|---|---|
| Invalid filename | 400 |
| Backup not found | 404 |
Generates a random 4-word passphrase from a curated word list (~960 words, ~10 bits per word, ~40 bits total entropy).
Response (200):
{ "passphrase": "wolf-hard-pore-jobs" }| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/health |
public | Basic health check |
| GET | /api/system/health |
socAuth | Detailed system health (public when soc_public=true) |
| GET | /api/fail2ban/status |
admin | Fail2Ban jail status |
| GET | /api/fail2ban/bans |
admin | Historical ban records from fail2ban DB |
Public liveness check.
Response (200):
{
"status": "ok",
"version": "1.2.0"
}Returns network connectivity (ICMP ping to 1.1.1.1), disk usage, CPU load, and scheduler status. Net/disk/cpu cached with 120s TTL. Scheduler status always fresh (atomic read).
Response (200):
{
"version": "3.3.1",
"net": {
"status": "ok",
"latency_ms": 12
},
"disk": {
"total_gb": 50.0,
"free_gb": 32.5
},
"cpu": {
"load1": 0.45,
"num_cpu": 4
},
"scheduler": {
"status": "ok",
"last_tick": "2026-03-22T10:05:00Z",
"active_checks": 243,
"stale_seconds": 32
}
}net.status: "ok" or "unreachable". When unreachable, latency_ms is -1.
scheduler.status: "ok" (last tick within 120s), "stale" (no tick for >120s), or "starting" (never ticked yet). When stale, audit log entries are written every 60s.
Requires fail2ban-client available via sudo on the server. Returns status of all Fail2Ban jails.
Response (200):
{
"jails": [
{
"name": "sshd",
"currently_failed": 2,
"total_failed": 15,
"currently_banned": 1,
"total_banned": 5,
"banned_ips": ["10.0.0.50"]
}
],
"fetched_at": "2026-01-15T10:00:00Z"
}| Error | Code |
|---|---|
| fail2ban not available | 503 |
| Command timed out (5s) | 504 |
Reads historical ban records from fail2ban's SQLite database (/var/lib/fail2ban/fail2ban.sqlite3).
Query params:
| Param | Type | Required | Description |
|---|---|---|---|
| jail | string | No | Filter by jail name |
Response (200):
{
"bans": [
{
"jail": "sshd",
"ip": "10.0.7.31",
"banned_at": "2026-03-10T10:39:36Z",
"expires_at": "2026-03-10T11:39:36Z",
"ban_count": 1
}
]
}| Error | Code |
|---|---|
| Invalid jail name | 400 |
| fail2ban DB not available | 503 |
Cookie-auth admin endpoints that mint and revoke bearer tokens consumed by /api/v1/* callers.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/api-tokens |
admin | List all tokens (active + revoked), metadata only |
| POST | /api/api-tokens |
admin | Mint a new token — plaintext is returned ONCE |
| DELETE | /api/api-tokens/{id} |
admin | Revoke a token (soft delete, idempotent) |
Response (200):
[
{
"id": "uuid",
"name": "grafana-prod",
"prefix": "bk_a1b2c3d4",
"created_by": "admin-user-uuid",
"created_at": "2026-04-24T09:00:00Z",
"last_used_at": "2026-04-24T09:12:45Z",
"revoked_at": null
}
]prefix is the first 11 characters of the plaintext ("bk_" + 8 hex); shown in the UI for identification. Hashed token values and plaintext are never returned here.
Request:
{ "name": "grafana-prod" }Name must be unique and 1–80 chars. Used only for identification in the admin UI.
Response (201):
{
"token": { /* same shape as a list row */ },
"plaintext": "bk_<64 hex chars>"
}Plaintext only appears in this response. The stored value is sha256-hashed; there is no way to retrieve it later. If the user loses it, they must issue a new token and revoke the old one.
| Error | Code |
|---|---|
| Missing/empty name | 400 |
| Duplicate name | 409 |
Soft-delete: sets revoked_at = now. Re-revoking is a no-op. Authentication attempts with the plaintext are rejected immediately (401 with WWW-Authenticate: Bearer error="invalid_token").
Response (200): { "status": "ok" }
| Error | Code |
|---|---|
| Token not found | 404 |
Machine-facing endpoints for remote consumers (monitoring dashboards, runbook scripts, SIEM integrations). Separate URL namespace, separate auth model: Authorization: Bearer bk_<...> — no cookie session required.
Base: /api/v1/. All endpoints in this namespace require a non-revoked bearer token from /api/api-tokens (admin-minted). Token-authenticated calls are logged at slog INFO with token_name, path, client_ip.
Design commitments:
- Versioned via URL. Breaking changes →
/api/v2/; thev1surface stays stable. - Admin-managed credentials (no end-user login). Tokens are revocable immediately.
- Response envelopes use arrays (
{"targets":[...]}) even for single-match lookups so callers don't need conditional decode paths.
Rate limiting: Flat per-token fixed-window, admin-tunable in Settings > General → API Rate Limit. Default 60 requests per minute per token. When the cap is hit, the server returns 429 Too Many Requests with a Retry-After header and a JSON body carrying retry_after_s and the current limit_per_min. Each 429 is logged at WARN (token_name, ip, path, limit_per_min) and recorded as an api_rate_limit_exceeded entry in the audit log (actor api-token:<name>). No IP-based limit is applied.
Return a point-in-time snapshot of any target(s) whose host field matches the query param. Two targets can legitimately share a host value; in that case, the response array carries one element per target.
Query params:
| Param | Required | Notes |
|---|---|---|
host |
Yes | Case-insensitive exact match against targets.host. No wildcards. |
Response (200):
{
"targets": [
{
"target_id": "uuid",
"name": "Web Server",
"host": "10.0.9.20",
"project": "DIAS",
"location": "DC-1",
"contacts": "ops@example.com",
"notes": "primary prod; owner alice",
"tags": ["IT", "P1"],
"last_check": {
"status": "up",
"check_type": "http",
"checked_at": "2026-04-24T09:12:45Z",
"message": "HTTP 200 (42ms)",
"response_ms": 42
}
}
]
}Field notes:
project,location,contacts,notesare strings. Empty string when unset (nevernull).tagsis always an array (empty[]when unset).last_checkreports the target's preferred check (itspreferred_check_type), falling back to the first check when the preferred type isn't configured. If no check has ever run,status: "none"with just thecheck_typecarried.last_check.status∈"up" | "down" | "none"."none"is distinct from"down"— it means "never observed" rather than "failed".
Empty result: unknown host returns 200 with {"targets": []} — not 404. Keeps the client code path uniform.
Examples:
curl -H "Authorization: Bearer bk_<token>" \
"http://bekci.internal:65000/api/v1/hosts?host=10.0.9.20"
| Error | Code |
|---|---|
Missing host param |
400 |
Missing or malformed Authorization: Bearer |
401 (+ WWW-Authenticate: Bearer realm="bekci-api") |
| Invalid or revoked token | 401 (+ WWW-Authenticate: Bearer error="invalid_token") |
| Per-token rate limit exceeded | 429 (+ Retry-After: <seconds>) — body: {"error":"rate limit exceeded","retry_after_s":N,"limit_per_min":M} |
Enabled only when cors_origin is configured (development). Allows methods: GET, POST, PUT, DELETE, OPTIONS. Allows headers: Content-Type. Sets Access-Control-Allow-Credentials: true for cookie-based auth. OPTIONS requests return 204 No Content.
- General endpoints: 1MB max (
readJSONhelper) - Backup restore: 2MB max
All HTTP requests are logged at DEBUG level with method, path, status code, and duration.