API Reference
Complete REST API documentation for agent communication, dashboard data access, and real-time WebSocket events.
Base URL
https://api.defensia.cloud/api/v1Authentication
Agent endpoints require Authorization: Bearer <token> (obtained at registration). Dashboard endpoints require Sanctum session authentication.
On this page
Agent Endpoints
/agents/registerRegister a new agent with the server. Returns an authentication token for subsequent requests.
Public — no bearer token required, but the body must carry a valid single-use install token.
Request body
{
"install_token": "<64-character install token>",
"name": "web-01",
"hostname": "web-01.example.com",
"ip_address": "1.2.3.4",
"os": "ubuntu",
"os_version": "22.04",
"version": "1.4.78"
} All seven fields are required. install_token must be exactly 64 characters, ip_address a valid IP. Send "unknown" for os_version on distributions whose /etc/os-release omits VERSION_ID — an empty string is rejected. Kubernetes nodes use /agents/register-k8s with a long-lived api_key instead.
Response 201
{
"token": "64-char-random-token",
"agent": {
"id": 1,
"name": "web-01",
"hostname": "web-01.example.com",
"ip_address": "1.2.3.4",
"os": "ubuntu",
"os_version": "22.04",
"version": "0.1.0",
"status": "offline"
},
"reverb": {
"url": "wss://ws.defensia.cloud/app/APP_KEY",
"app_key": "APP_KEY",
"auth_endpoint": "https://api.defensia.cloud/api/v1/agent/broadcasting/auth"
}
}Errors
422(field errors)A required field is missing or malformed. The body lists each rejected field with its reason; a mis-pasted install token is the usual cause.401token_not_foundNo install token matches. Generate a new one and reinstall.401token_expiredThe token was already used or is older than one hour. Tokens are single-use.403(none)Server limit reached — the organization has no free slot on its current plan.429rate_limitedA capacity budget is full. Wait for the window and resend the same request — the token is not the problem and does not need regenerating.429invalid_credential_rate_limitedToo many refused credentials from this address. Wait, then retry with a freshly generated token.Rate limits
Registration is bounded independently per credential and per source address. A 429 names the budget it hit in scope, and the wait in both retry_after and the Retry-After header.
15406030060300—10Only the last budget counts attempts whose credential was refused, so a request carrying a valid token never draws on it — an earlier typo cannot lock out a correct token. A 4xx other than 429 is permanent: the same payload will be rejected identically, so retry only after fixing the field the response names.
/agent/heartbeat Send a heartbeat to keep the agent marked as online. Requires Authorization: Bearer <token>
Request body
{
"status": "online",
"version": "0.1.0",
"timestamp": "2026-02-19T14:00:00Z"
}status values:onlineofflinewarning
Response 200
{
"status": "online",
"last_seen_at": "2026-02-19T14:00:00.000000Z",
"latest_agent_version": "0.2.0",
"agent_download_base_url": "https://github.com/defensia/agent/releases/download/v0.2.0"
}latest_agent_version and agent_download_base_url are only present when an admin has configured an agent version. The agent uses these to trigger auto-updates.
/agent/events Submit a batch of security events detected by the agent. Requires Authorization: Bearer <token>
Request body
{
"events": [
{
"type": "brute_force",
"severity": "warning",
"source_ip": "5.5.5.5",
"source_port": 54321,
"target_port": 22,
"protocol": "tcp",
"details": { "attempts": 5, "user": "root" },
"occurred_at": "2026-02-19T14:00:00Z"
}
]
}type values
brute_forceport_scanfloodintegrity_changezombie_processesseverity values
infowarningcriticalResponse 201
{ "stored": 1 }/agent/sync Returns the full agent configuration, active bans, rules, whitelists, and threat feed data. Includes brute force settings and geoblocking countries in the config object. Called once at startup, then use Reverb WebSocket for real-time updates. Requires Authorization: Bearer <token>
Response 200
{
"config": {
"bf_threshold": 5,
"bf_window": 300,
"bf_ban_duration": null,
"blocked_countries": ["CN", "RU", "KP"]
},
"rules": [
{
"id": 1,
"type": "block",
"protocol": "tcp",
"ip_address": "10.0.0.5",
"port": null,
"priority": 100
}
],
"bans": [
{
"id": 1,
"ip_address": "5.5.5.5",
"reason": "brute_force_ssh",
"expires_at": null
}
],
"whitelists": [
{ "id": 1, "ip_address": "192.168.1.1", "ip_range": null }
],
"threat_feed": [],
"agent_update": {
"latest_version": "0.2.0",
"download_base_url": "https://github.com/defensia/agent/releases/download/v0.2.0"
}
}threat_feed is only populated when network intelligence is enabled (Pro). agent_update is null when no agent version has been configured by an admin.
/agent/bans Report a ban applied by the agent. The server stores it and broadcasts it to the dashboard. Requires Authorization: Bearer <token>
Request body
{
"ip_address": "5.5.5.5",
"reason": "brute_force_ssh",
"ban_count": 6,
"expires_at": null
}Response 201
Returns the created ban object.
/agent/broadcasting/auth Authenticate an agent for Reverb private channel subscription. Requires Authorization: Bearer <token> and Content-Type: application/x-www-form-urlencoded
Request body
socket_id=xxx.xxx&channel_name=private-agent.{id}Response 200
{ "auth": "APP_KEY:HMAC_SHA256_SIGNATURE" }Dashboard Endpoints
All dashboard endpoints require Sanctum session authentication.
/statsReturns aggregated statistics for the dashboard overview.
Response 200
{
"servers": { "total": 5, "online": 3, "offline": 1, "warning": 1 },
"events": { "today": 42, "critical": 3, "total": 1500 },
"bans": { "active": 12, "total": 340 }
}/servers/servers/{id}GET /servers returns a paginated list of servers. GET /servers/{id} returns a single server with its events, bans, and rules.
/eventsReturns a paginated list of security events, latest first.
/bans/bans/bans/{id} Manage IP bans. GET returns paginated active bans. POST creates a manual ban. DELETE unbans an IP (sets is_active=false).
POST /bans body
{
"ip_address": "1.2.3.4",
"reason": "manual block",
"expires_at": "2026-03-01T00:00:00Z"
}Malware Endpoints
Scan results are read-only. Remediation — ignore, quarantine, restore — stays in the dashboard on purpose: those actions change what runs on your server.
/servers/{id}/malware-scans/servers/{id}/malware-scans/{scan}Scans for one server, newest first. A scan belonging to a different server returns 404 even inside your own organisation, so scan ids cannot be used to enumerate servers you do not own.
/servers/{id}/malware-scans/{scan}/findings The files a scan flagged, worst severity first — the same rows the dashboard CSV export carries, plus signature_id, framework and match_line.
Query parameters
status detected | suspicious | quarantined | ignored severity critical | high | medium | low per_page default 100, max 500
detected is flagged with no action taken. suspicious matched but fell below the confidence needed to call it malware. quarantined was moved out of the web root by the agent. ignored was allow-listed by a user and is kept so it is not reported again.
An unknown value returns 422 naming the field, rather than an empty list — a wrong filter should never look like a clean scan.
/servers/{id}/malware-scans/{scan}/framework-findings Misconfigurations rather than malware: a world-readable wp-config.php, debug mode left on in production, a PHP file where only uploads belong. Separate from /findings on purpose — a bad file permission is not an infection, and merging the two would misreport both.
Query parameters
check_id e.g. WP_CONFIG_PERMS, WP_DEBUG_ENABLED severity critical | high | medium | low per_page default 100, max 500
Each row carries check_id, title, severity, description, file_path, domain and framework.
/servers/{id}/malware-scan-config/servers/{id}/malware-scan-config Read or change the scan schedule, the paths scanned, and the paths never scanned. The PATCH is partial: sending exclude_paths alone leaves your schedule untouched. Changes reach the agent on its next sync, within about a minute — the response says so with applies_on_next_sync.
Exclusion patterns
wp-content/wflogs a path segment — matches under every web root /var/log/app an absolute path — and everything beneath it *.log, /home/*/cache a glob — full path or base name
Exclusions apply to the scheduled scan and to real-time scanning. The bare-segment form is usually the one you want: one entry covers the same directory across every site on the server. Blank entries are discarded, so a stray empty line cannot exclude everything.
Example
curl -X PATCH https://defensia.cloud/api/v1/servers/176/malware-scan-config \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"exclude_paths": ["wp-content/wflogs"]}'WebSocket Events
Connect to wss://ws.defensia.cloud/app/{APP_KEY} using the Pusher protocol (Laravel Reverb).
private-dashboardSubscribes web dashboard users. Receives all events across all agents.
ban.created{ id, ip_address, reason, source, expires_at, agent_id }ban.removed{ id, ip_address, agent_id }agent.status{ id, name, hostname, status, version, last_seen_at }event.received{ id, agent_id, type, severity, source_ip, occurred_at }private-agent.{id}Subscribes agents via bearer token auth. Receives only agent-specific events.
ban.created{ id, ip_address, reason, expires_at }ban.removed{ id, ip_address }