When the --listen flag is used (e.g., --listen :8080), the bot-detector starts an internal web server to provide live metrics and access to the current configuration.
This API is intended for monitoring and administrative purposes.
The --listen flag supports multiple listeners with optional role-based routing. API endpoints can be isolated using role=api:
# Dedicated API listener
--listen :8080,role=api
# API and metrics on separate ports
--listen :8080,role=api --listen :9090,role=metricsSee the main README.md for complete --listen flag documentation and routing rules.
- Method:
GET - Content-Type:
text/plain; charset=utf-8 - Description: Displays a comprehensive plain-text report of the application's real-time metrics. This is the main dashboard for monitoring activity. The report includes:
- Timestamp and uptime information
- General processing statistics (lines processed, valid hits, errors, processing rate)
- Actor statistics (good actors skipped, actors cleaned)
- Chain and action statistics
- Per-chain metrics (hits, completions, resets) - only active chains shown
- Website information for multi-website mode (shown as
[website1, website2]after chain name)
- Method:
GET - Content-Type:
text/plain; charset=utf-8 - Description: Provides a plain text report of behavioral chain step executions grouped by website. The report includes:
- Timestamp
- Total step executions across all websites
- Per-website sections showing:
- Website name and execution count for that website
- Individual step counts with percentages (calculated against overall total)
- Steps sorted by execution count in descending order
- Global chains shown first, then website-specific chains alphabetically
- Step names include website/vhost context (e.g.,
step 1/3 of ChainName[website]) - Useful for debugging chain performance and comparing activity across websites.
- Method:
GET - Content-Type:
text/plain; charset=utf-8 - Description: Displays multi-website statistics including configured websites, their vhosts, log paths, chain assignments, and a list of unknown vhosts encountered in logs. The report includes:
- Timestamp
- Website configuration details
- Per-website metrics (lines parsed, chain matches, completions, resets)
- Unknown vhosts list
- This endpoint is particularly useful for:
- Verifying multi-website configuration
- Identifying misconfigured or missing vhosts
- Troubleshooting log entries that are being skipped
- Response (Multi-Website Mode):
Generated: 2026-03-10T09:15:00+01:00 === Multi-Website Statistics === Total Websites: 2 Global Chains: 1 Website-Specific Chains: 2 === Configured Websites === main: VHosts: www.example.com, example.com Log Path: /var/log/haproxy/main.log Chains: 1 api: VHosts: api.example.com Log Path: /var/log/haproxy/api.log Chains: 1 --- Unknown VHosts --- Total: 1 VHosts: - unknown.example.com Note: Unknown vhosts are logged once and their entries are skipped. To fix: Add the vhost to a website's 'vhosts' list in config.yaml - Response (Single-Website Mode):
Multi-website mode is not enabled. To enable, add a 'websites' section to your config.yaml
- Method:
GET - Content-Type:
text/yaml; charset=utf-8 - Description: Returns the raw YAML content of the main configuration file as it was last loaded by the application. This allows you to inspect the exact configuration that is currently active, which is especially useful after a hot-reload.
- Responses:
200 OK: Successfully returns the YAML configuration.500 Internal Server Error: If the server fails to retrieve or marshal the configuration content.
- Method:
GET - Content-Type:
application/gzip - Description: Downloads a
.tar.gzarchive containing the activeconfig.yamland all of its file-based dependencies (e.g., files referenced withfile:). This provides a complete, portable snapshot of a working configuration, which can be used for backups or for migrating the exact same ruleset to another bot-detector instance. The archive preserves the relative directory structure of the dependencies. - Responses:
200 OK: Successfully begins streaming the gzipped tar archive.500 Internal Server Error: If the server fails to access the configuration or its dependencies to create the archive.
These endpoints are available when cluster mode is enabled. They provide cluster status, metrics collection, and aggregation capabilities.
- Method:
GET - Content-Type:
application/json - Description: Returns the current node's cluster identity and role information. This endpoint is useful for determining which node you're connected to and its role in the cluster.
- Response Format (Leader):
{ "role": "leader", "name": "node-1", "address": "localhost:8080" } - Response Format (Follower):
{ "role": "follower", "name": "node-2", "address": "localhost:9090", "leader": "node-1:8080" } - Responses:
200 OK: Successfully returns the node status.500 Internal Server Error: If the server fails to retrieve node status information.
- Method:
GET - Content-Type:
application/json - Description: Returns this node's current metrics snapshot in JSON format. This endpoint is used by leader nodes to collect metrics from follower nodes, but can also be queried directly for monitoring individual nodes. The metrics include processing statistics, actor statistics, chain execution statistics, per-website statistics (in multi-website mode), and various performance counters.
- Response Format:
{ "timestamp": "2025-11-18T20:30:00Z", "processing_stats": { "lines_processed": 1000, "entries_checked": 42, "parse_errors": 1, "reordered_lines": 2, "time_elapsed_seconds": 10.5, "lines_per_second": 95.24 }, "actor_stats": { "good_actors_skipped": 10, "actors_cleaned": 5 }, "chain_stats": { "actions_block": 15, "actions_log": 27, "total_hits": 695, "completed": 42, "resets": 1 }, "good_actor_hits": { "known_bot": 4, "monitoring_agent": 2, "our_network": 4 }, "skips_by_reason": { "blocked:SimpleBlockChain": 2, "good_actor:known_bot": 4 }, "match_key_hits": { "ip": 652, "ip_ua": 41, "ipv6": 2 }, "block_durations": { "1h": 1, "30m": 14 }, "per_chain_metrics": { "SimpleBlockChain": { "hits": 2, "completed": 1, "resets": 0 }, "SimpleLogChain": { "hits": 4, "completed": 2, "resets": 0 } }, "website_metrics": { "main_site": { "lines_parsed": 450, "chains_matched": 12, "chains_reset": 1, "chains_completed": 8 }, "api_site": { "lines_parsed": 550, "chains_matched": 30, "chains_reset": 0, "chains_completed": 34 } } } - Note: The
website_metricsfield is only present when multi-website mode is enabled. - Responses:
200 OK: Successfully returns the metrics snapshot.500 Internal Server Error: If the server fails to generate the metrics snapshot.
- Method:
GET - Content-Type:
application/json - Description: Returns cluster-wide aggregated metrics from all nodes (leader only). This endpoint provides a comprehensive view of the entire cluster's performance, including per-node health status, cluster-wide metric summation, and per-website aggregates (in multi-website mode). Only available on leader nodes; follower nodes will return a 404 error.
- Response Format:
{ "timestamp": "2025-11-18T20:30:00Z", "total_nodes": 3, "healthy_nodes": 2, "stale_nodes": 0, "error_nodes": 1, "aggregated": { "timestamp": "2025-11-18T20:30:00Z", "processing_stats": { "lines_processed": 3000, "entries_checked": 126, "parse_errors": 3, "reordered_lines": 6, "time_elapsed_seconds": 31.5, "lines_per_second": 95.24 }, "actor_stats": { "good_actors_skipped": 30, "actors_cleaned": 15 }, "chain_stats": { "actions_block": 45, "actions_log": 81, "total_hits": 2085, "completed": 126, "resets": 3 }, "good_actor_hits": { "known_bot": 12, "monitoring_agent": 6, "our_network": 12 }, "per_chain_metrics": { "SimpleBlockChain": { "hits": 6, "completed": 3, "resets": 0 } }, "website_metrics": { "main_site": { "lines_parsed": 1350, "chains_matched": 36, "chains_reset": 3, "chains_completed": 24 }, "api_site": { "lines_parsed": 1650, "chains_matched": 90, "chains_reset": 0, "chains_completed": 102 } } }, "nodes": [ { "node_name": "follower-1", "address": "localhost:9090", "status": "healthy", "last_collected": "2025-11-18T20:29:55Z", "consecutive_errors": 0, "metrics": { "timestamp": "2025-11-18T20:29:55Z", "processing_stats": { "lines_processed": 1000, "entries_checked": 42 } } }, { "node_name": "follower-2", "address": "localhost:9091", "status": "healthy", "last_collected": "2025-11-18T20:29:54Z", "consecutive_errors": 0, "metrics": { "timestamp": "2025-11-18T20:29:54Z", "processing_stats": { "lines_processed": 1000, "entries_checked": 42 } } }, { "node_name": "follower-3", "address": "localhost:9092", "status": "error", "last_collected": "2025-11-18T20:25:10Z", "consecutive_errors": 5, "last_error": "HTTP request failed: connection refused" } ] } - Node Health Status:
"healthy": Node is responding normally and metrics are fresh (within 3x the poll interval)"stale": Node metrics are outdated (last collection > 3x poll interval)"error": Node has consecutive errors or no snapshot available
- Responses:
200 OK: Successfully returns aggregated cluster metrics.404 Not Found: This endpoint is only available on leader nodes. Returned when querying a follower node.500 Internal Server Error: If the server fails to aggregate metrics.
These endpoints allow you to query the block/unblock status of specific IP addresses. They provide visibility into which IPs are currently blocked, why they were blocked, and when blocks will expire.
-
Method:
GET -
Content-Type:
text/plain; charset=utf-8 -
Description: Returns the block/unblock status of an IP address. This endpoint is cluster-aware and automatically provides the appropriate view based on deployment:
- Follower nodes: Forward the request to the leader and return cluster-wide aggregated status
- Leader nodes: Query all nodes and return cluster-wide aggregated status
- Standalone nodes: Return local node status only
The IP address is automatically canonicalized (e.g.,
2001:0db8::1becomes2001:db8::1). -
Parameters:
ip- IPv4 or IPv6 address (will be canonicalized)
-
Response Format (Cluster - Blocked):
cluster_status: blocked nodes: - name: node-1 status: blocked actors: 2 chains: - SimpleBlockChain (until: 2025-11-22T02:00:00Z) persistence: blocked persistence_expires: 2025-11-22T02:00:00Z - name: node-2 status: unknown backend_tables: - thirty_min_blocks_v4 on /var/run/haproxy/admin.sock (status: blocked, duration: 30m, expires in: 15m, added: 2025-11-28 10:30:00) -
Response Format (Standalone - Blocked):
node: standalone status: blocked actors: 2 chains: - SimpleBlockChain (until: 2025-11-22T02:00:00Z) persistence: blocked persistence_expires: 2025-11-22T02:00:00Z backend_tables: - thirty_min_blocks_v4 on /var/run/haproxy/admin.sock (status: blocked, duration: 30m, expires in: 15m) -
Response Format (Unknown IP):
cluster_status: unknown nodes: - name: node-1 status: unknown - name: node-2 status: unknown -
Cluster Status Values:
"blocked": IP is blocked on all nodes that have information about it"unblocked": IP is not blocked on any node"unknown": IP is not known to any node"mixed": IP has different statuses across nodes
-
Notes:
- Followers automatically get cluster-wide view by forwarding to leader
- Leader queries all nodes concurrently (5-second timeout per node)
- Provides complete picture of IP status across entire deployment
- Shows persistence state from each node
- The
actorsfield indicates how many IP+UserAgent combinations exist for this IP - Multiple chains can block the same IP if different behavioral patterns are detected
-
Responses:
200 OK: Successfully returns IP status.400 Bad Request: Invalid IP address format.502 Bad Gateway: (Follower only) Failed to contact leader.
-
Method:
GET -
Content-Type:
application/json -
Description: Returns the block/unblock status of an IP address in JSON format. This endpoint is cluster-aware and designed for programmatic access:
- Follower nodes: Forward the request to the leader and return cluster-wide aggregated status
- Leader nodes: Query all nodes and return cluster-wide aggregated status
- Standalone nodes: Return local node status only
-
Parameters:
ip- IPv4 or IPv6 address (will be canonicalized)
-
Response Format (Cluster - Blocked):
{ "cluster_status": "blocked", "nodes": [ { "name": "node-1", "status": "blocked", "actors": 2, "chains": { "SimpleBlockChain": "2025-11-22T02:00:00Z" }, "persistence": "blocked", "persistence_expires": "2025-11-22T02:00:00Z" }, { "name": "node-2", "status": "unknown" } ] } -
Response Format (Standalone - Blocked IP):
{ "node": "standalone", "status": "blocked", "actors": 2, "chains": { "SimpleBlockChain": "2025-11-22T02:00:00Z", "API-Abuse-Chain": "2025-11-22T01:30:00Z" }, "earliest_block": "2025-11-22T01:00:00Z", "latest_expiry": "2025-11-22T02:00:00Z", "persistence": "blocked", "persistence_expires": "2025-11-22T02:00:00Z" } -
Response Format (Standalone - Unblocked IP):
{ "node": "standalone", "status": "unblocked", "last_seen": "2025-11-22T01:00:00Z", "last_unblock": "2025-11-22T00:30:00Z", "unblock_reason": "good-actor:monitoring_agent" } -
Response Format (Unknown IP):
{ "cluster_status": "unknown", "nodes": [ { "name": "node-1", "status": "unknown" } ] } -
Notes:
- All timestamps are in RFC3339 format (ISO 8601)
- IPv6 addresses are canonicalized (e.g.,
2001:0db8::1→2001:db8::1) - Followers automatically get cluster-wide view by forwarding to leader
- Leader queries all nodes concurrently (5-second timeout per node)
- Standalone nodes return local
IPStatusResponseformat - Cluster nodes return
ClusterIPAggregateResponseformat
-
Responses:
200 OK: Successfully returns IP status.400 Bad Request: Invalid IP address format.502 Bad Gateway: (Follower only) Failed to contact leader.
}
-
Error Response (400 Bad Request):
{ "error": "Invalid IP address" } -
Notes:
- All timestamps are in RFC3339 format (ISO 8601)
- IPv6 addresses are canonicalized (e.g.,
2001:0db8::1→2001:db8::1) - The
chainsobject maps chain names to their expiry times - The
cluster_hintfield (on followers) provides the URL to query for cluster-wide status
-
Responses:
200 OK: Successfully returns IP status.400 Bad Request: Invalid IP address format.
- Method:
DELETE - Content-Type:
text/plain; charset=utf-8 - Description: Clears an IP address from all state: HAProxy stick tables, activity store, and persistence (journal/snapshot). This is a cluster-aware operation - if called on a follower, the request is forwarded to the leader, which then broadcasts the clear command to all nodes. Each node independently clears the IP from its local HAProxy tables and persistence state. An unblock event is written to the journal with reason "manual_clear".
- Parameters:
ip- IPv4 or IPv6 address (will be canonicalized)
- Response Format (Success - IP found):
IP 192.168.1.100 found and cleared from: - thirty_min_blocks_v4 on /var/run/haproxy/admin.sock (status: blocked, duration: 30m, expires in: 15m, added: 2025-11-28 10:30:00) - one_hour_blocks_v4 on /var/run/haproxy/admin.sock (status: blocked, duration: 1h, expires in: 45m, added: 2025-11-28 10:00:00) - Response Format (Success - IP not found):
IP 192.168.1.100 not found in any tables - Error Response (400 Bad Request):
Invalid IP address - Error Response (502 Bad Gateway - Follower only):
Failed to contact leader: connection refused - Error Response (503 Service Unavailable):
Blocker not available - Error Response (500 Internal Server Error):
Failed to clear IP: <error details> - Cluster Behavior:
- Follower nodes: Forward the request to the leader and return the leader's response
- Leader node: Clear locally, then broadcast to all followers asynchronously
- Standalone node: Clear locally only
- What gets cleared:
- All HAProxy stick table entries for the IP (across all duration tables) - removed completely
- IP from persistence state (
IPStatesmap) - Unblock event written to journal with reason "manual_clear"
- Performance Note:
- Removing entries from HAProxy tables is slow for large tables
- For quick unblocking, use
/ip/{ip}/unblockinstead (setsgpc0=0, much faster)
- Notes:
- The IP address is canonicalized before processing (e.g.,
2001:0db8::1→2001:db8::1) - Clears from all configured HAProxy instances and all duration tables
- Works for both blocked and unblocked IPs (clears all state regardless of status)
- The operation is logged on each node that processes it
- Broadcast to followers is asynchronous (fire-and-forget) - leader doesn't wait for follower responses
- If a follower is unreachable during broadcast, it's logged but doesn't fail the request
- Use this when you want to completely remove an IP from the system (e.g., false positive, testing)
- The IP address is canonicalized before processing (e.g.,
- Responses:
200 OK: Successfully cleared the IP (or IP not found).400 Bad Request: Invalid IP address format.500 Internal Server Error: Failed to clear the IP from HAProxy or persistence.502 Bad Gateway: (Follower only) Failed to forward request to leader.503 Service Unavailable: Blocker is not available (e.g., dry-run mode).
- Method:
GETorPOST - Content-Type:
text/plain; charset=utf-8(POST) or cluster status format (GET) - Description: Fast unblock operation that sets
gpc0=0in HAProxy stick tables without removing the entry. The entry remains in the table and expires naturally. This is much faster than/clearfor large tables. Cluster-aware - forwards to leader if called on follower, then broadcasts to all nodes. - Parameters:
ip- IPv4 or IPv6 address (will be canonicalized)
- Response Format (POST - Simple confirmation):
IP 192.168.1.100 unblocked (gpc0 set to 0, entry will expire naturally) - Response Format (GET - Unblock + Status):
cluster_status: unblocked nodes: - name: node1 status: unblocked last_unblock: 2026-03-10T15:09:00Z reason: API unblock - name: node2 status: unblocked last_unblock: 2026-03-10T15:09:00Z reason: API unblock - Cluster Behavior:
- Follower nodes: Forward the request to the leader (preserves GET/POST method)
- Leader node: Unblock locally, then broadcast to all followers asynchronously
- Standalone node: Unblock locally only
- What gets updated:
- HAProxy stick tables:
gpc0set to 0 (entry remains, expires naturally) - Persistence state: Unblock event written to journal with reason "API unblock"
- Activity store: IP removed from in-memory chain progress
- HAProxy stick tables:
- Performance:
- Fast: Only updates
gpc0value, doesn't remove entry from table - Recommended for day-to-day unblocking operations
- Entry will be removed by HAProxy when it naturally expires
- Fast: Only updates
- Usage Examples:
# Quick unblock with status confirmation (GET) curl http://localhost:8092/ip/192.168.1.100/unblock # Simple unblock (POST) curl -X POST http://localhost:8092/ip/192.168.1.100/unblock
- Comparison with
/clear:/unblock: Setsgpc0=0, entry expires naturally (fast)/clear: Removes entry completely from table (slow)
- Responses:
200 OK: Successfully unblocked the IP.400 Bad Request: Invalid IP address format.500 Internal Server Error: Failed to unblock the IP.502 Bad Gateway: (Follower only) Failed to forward request to leader.503 Service Unavailable: Blocker is not available (e.g., dry-run mode).
- Method:
GET - Content-Type:
application/json - Description: Internal endpoint used by the leader to query follower nodes for IP status. This endpoint returns the same information as
/api/v1/ip/{ip}but without thenodeandcluster_hintfields, as these are added by the leader during aggregation. This endpoint is not intended for direct user access. - Parameters:
ip- IPv4 or IPv6 address (will be canonicalized)
- Response Format: Same as
/api/v1/ip/{ip}but withoutnodeandcluster_hintfields. - Responses:
200 OK: Successfully returns IP status.400 Bad Request: Invalid IP address format.
# Works on any node - automatically provides cluster-wide view in cluster mode
curl http://localhost:8080/ip/192.168.1.100# Returns cluster aggregation on leader/follower, local view on standalone
curl http://localhost:8080/api/v1/ip/192.168.1.100curl -X DELETE http://localhost:8080/ip/192.168.1.100/clearcurl -X DELETE http://localhost:8080/ip/2001:db8::1/clearcurl http://localhost:8080/ip/2001:0db8::1
# Queries for canonical form: 2001:db8::1#!/bin/bash
IP="192.168.1.100"
STATUS=$(curl -s "http://localhost:8080/api/v1/ip/$IP" | jq -r '.status')
if [ "$STATUS" = "blocked" ]; then
echo "IP $IP is currently blocked"
exit 1
else
echo "IP $IP is not blocked"
exit 0
fi#!/bin/bash
IP="192.168.1.100"
STATUS=$(curl -s "http://localhost:8080/api/v1/ip/$IP" | jq -r '.status')
if [ "$STATUS" = "blocked" ]; then
echo "IP $IP is blocked, clearing..."
curl -X DELETE "http://localhost:8080/ip/$IP/clear"
else
echo "IP $IP is not blocked"
fi#!/bin/bash
IP="192.168.1.100"
RESPONSE=$(curl -s "http://localhost:8080/api/v1/ip/$IP")
PERSISTENCE=$(echo "$RESPONSE" | jq -r '.persistence // "none"')
if [ "$PERSISTENCE" != "none" ]; then
echo "IP $IP is in persistence state: $PERSISTENCE"
EXPIRES=$(echo "$RESPONSE" | jq -r '.persistence_expires // "unknown"')
echo "Expires: $EXPIRES"
else
echo "IP $IP is not in persistence"
fi