REST API
flowwler exposes a REST API on the same HTTP server as the Prometheus metrics endpoint (default [::]:9731, configured via http.listen_addr).
All responses use Content-Type: application/json. Error responses have the form:
{"error": "message"}
Interactive reference: Swagger UI - open in a browser for a fully interactive version of this documentation with live try-it-out support.
Machine-readable spec: assets/openapi.yaml - OpenAPI 3.0.3.
Authentication
Authentication is optional. When not configured all endpoints are open. The /metrics endpoint is never gated regardless of which method is configured.
Two methods are available and can be enabled independently; if both are configured, either is accepted.
API key
Set http.auth.api_key in the config. Supply the key on every request via one of:
Authorization: Bearer <key>
X-API-Key: <key>
HTTP Basic Auth
Set http.auth.username and http.auth.password_hash. Generate the hash with:
flowwler hash-password
Password: ········
$2a$10$...
Paste the printed hash into the config:
http:
auth:
username: "admin"
password_hash: "$2a$10$..."
Supply credentials on every request via standard HTTP Basic Auth:
curl -u admin:mypassword http://localhost:9731/api/v1/status
The password is hashed with bcrypt (cost 10). The hash is re-evaluated on every request so a SIGHUP reload takes effect immediately.
Endpoints
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/status |
Daemon version, dry-run flag, and uptime |
GET |
/api/v1/routers |
All known flow exporters with up/down status and sampling rate |
GET |
/api/v1/prefix-sources |
External prefix source resolution state per group (IRR, NetBox, URL subnets, whitelists) |
POST |
/api/v1/reload |
Trigger a live config reload (equivalent to SIGHUP) |
POST |
/api/v1/reload/sources |
Re-fetch all external prefix sources (IRR, NetBox, URL subnets, whitelists) without a full config reload |
GET |
/api/v1/config |
Sanitized summary of the active configuration |
GET |
/api/v1/mitigations |
List all active mitigations |
GET |
/api/v1/mitigations/{ip} |
List active mitigations for a specific victim IP |
POST |
/api/v1/mitigations |
Manually activate a mitigation |
DELETE |
/api/v1/mitigations/ip/{ip} |
Withdraw all mitigations for a victim IP regardless of type |
DELETE |
/api/v1/mitigations/{type}/{ip} |
Withdraw all mitigations of a specific type for a victim IP |
DELETE |
/api/v1/mitigations/uuid/{uuid} |
Withdraw a mitigation (blackhole, subnet-blackhole, or FlowSpec rule) by UUID |
GET |
/api/v1/escalations |
Full escalation engine state for all victims |
GET |
/api/v1/escalations/{group}/{ip} |
Escalation state for a specific victim IP within a group |
GET |
/api/v1/bgp/peers |
BGP peer session status |
GET |
/api/v1/bgp/routes |
BGP global RIB across all address families |
GET |
/api/v1/groups |
List all groups with traffic rates and escalation status |
GET |
/api/v1/groups/{group} |
Single group detail |
GET |
/api/v1/groups/{group}/subnets |
All resolved subnets for a group (static + IRR/NetBox) |
GET |
/api/v1/groups/{group}/rules |
Rules for a group with match counts and escalation status |
GET |
/api/v1/groups/{group}/targets |
Current per-IP traffic rates for all targets in a group |
GET |
/api/v1/flowspec/rules |
All active FlowSpec rules - auto-managed and manual |
GET |
/api/v1/notifications/targets |
Notification target health and last-fire status |
POST |
/api/v1/notifications/targets/{name}/test |
Send a test notification to a specific target |
GET |
/api/v1/attacks |
Attack history (SQLite) with optional filters |
GET |
/api/v1/attacks/stats |
Aggregate statistics across all stored attacks |
GET |
/api/v1/attacks/{attack_id} |
Full detail for one attack session including all events |
GET |
/api/v1/ip/{ip} |
Resolve an IP to its group and matched subnet |
GET |
/api/v1/flowspec/suggest/{ip} |
Live FlowSpec rule suggestions for a victim IP based on current flow data |
POST |
/api/v1/escalations/{group}/{ip}/clear |
Force all escalation states for a victim IP to idle (clears hold-down immediately) |
GET |
/api/v1/whitelist |
List all whitelist scopes with prefix counts |
GET |
/api/v1/whitelist/prefixes |
List all CIDR prefixes for every whitelist scope |
GET |
/api/v1/pcap/captures |
List currently open per-attack PCAP capture sessions |
GET |
/api/v1/pcap/attacks |
List attacks that have an associated PCAP capture, with file metadata and packet preview |
GET |
/api/v1/pcap/attacks/{attack_id} |
Download the PCAP file for an attack |
POST |
/api/v1/pcap/manual |
Start an ad-hoc PCAP capture for any victim IP |
GET |
/api/v1/pcap/manual |
List active manual captures |
GET |
/api/v1/pcap/manual/{id} |
Download the PCAP file for a manual capture |
DELETE |
/api/v1/pcap/manual/{id} |
Stop a manual capture early |
Common workflows
Emergency block a single IP
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{"type":"blackhole","victim_ip":"198.51.100.1"}' | jq .
Block a subnet
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{"type":"subnet-blackhole","victim_ip":"198.51.100.1"}' | jq .
Rate-limit UDP/53 traffic toward a victim
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{
"type": "flowspec",
"victim_ip": "198.51.100.1",
"action": "rate-limit",
"rate_limit_bps": 10000000,
"match": {"protocol": 17, "dst_ports": [53]}
}' | jq .
Remove all mitigations for a victim at once
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/ip/198.51.100.1 | jq .
List what is active, then remove one item
# See all active mitigations
curl -s http://[::]:9731/api/v1/mitigations | jq .
# Remove the blackhole for a specific victim
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/blackhole/198.51.100.1 | jq .
Verify BGP is up before a maintenance window
curl -s http://[::]:9731/api/v1/bgp/peers | jq '.[].SessionState'
GET /api/v1/status
Returns daemon metadata: version, dry-run mode, start time, uptime, and the outcome of the most recent config reload (SIGHUP or POST /api/v1/reload).
curl -s http://[::]:9731/api/v1/status | jq .
Response
{
"version": "v1.1.3",
"dry_run": false,
"started_at": "2026-03-25T10:00:00Z",
"uptime_seconds": 3600.5,
"last_reload": {
"trigger": "API",
"success": false,
"error": "validation: gobgp: local_asn is required",
"completed_at": "2026-03-25T10:15:02Z"
}
}
last_reload is omitted until the first reload attempt since the daemon started. POST /api/v1/reload only triggers a reload and always returns 202 immediately (reload runs on the main goroutine) — poll GET /api/v1/status afterward to confirm it actually succeeded; on failure the daemon keeps running with the previous config and last_reload.error explains why. trigger is "SIGHUP" or "API".
GET /api/v1/routers
Returns all flow exporters (NetFlow/IPFIX/sFlow) that have sent at least one packet, with their current liveness status and last-observed sampling rate.
curl -s http://[::]:9731/api/v1/routers | jq .
Response
[
{
"router": "192.0.2.1",
"protocol": "netflow",
"up": true,
"last_seen": "2026-03-25T10:59:55Z",
"sampling_rate": 1000
}
]
| Field | Description |
|---|---|
router |
Router IP address |
protocol |
netflow, netflow5, ipfix, or sflow |
up |
true while packets received within the last 120 seconds |
last_seen |
RFC 3339 timestamp of the last received packet |
sampling_rate |
Most recently observed 1-in-N sampling rate; 0 = unknown |
GET /api/v1/prefix-sources
Returns the last-known resolution state for every group that has an external prefix source configured (IRR, NetBox, or URL subnet imports).
curl -s http://[::]:9731/api/v1/prefix-sources | jq .
Response
[
{
"group": "customer-a",
"source": "irr",
"ipv4_routes": 420,
"ipv6_routes": 85,
"asn_count": 12,
"resolve_duration_seconds": 1.23,
"last_refresh_at": "2026-03-25T08:00:00Z"
},
{
"group": "customer-b",
"source": "netbox",
"ipv4_routes": 50,
"ipv6_routes": 10,
"resolve_duration_seconds": 0.45,
"last_refresh_at": "2026-03-25T08:00:01Z"
},
{
"group": "customer-c",
"source": "url",
"ipv4_routes": 120,
"ipv6_routes": 0,
"resolve_duration_seconds": 0.18,
"last_refresh_at": "2026-03-25T08:00:02Z"
},
{
"group": "defaults",
"source": "url-whitelist",
"ipv4_routes": 30,
"ipv6_routes": 5,
"resolve_duration_seconds": 0.09,
"last_refresh_at": "2026-03-25T08:00:02Z"
}
]
| Field | Description |
|---|---|
group |
Group name. For whitelist URL imports the value is "defaults" or "group/<name>" to distinguish them from victim-subnet imports. |
source |
irr, netbox, url (group victim-subnet URL import), or url-whitelist (whitelist URL import) |
ipv4_routes |
Number of IPv4 prefixes in the last resolution |
ipv6_routes |
Number of IPv6 prefixes in the last resolution |
asn_count |
Number of ASNs expanded (IRR only) |
resolve_duration_seconds |
Wall-clock seconds taken by the last resolution |
last_refresh_at |
RFC 3339 timestamp of the last successful resolution |
POST /api/v1/reload
Triggers a live configuration reload identical to sending SIGHUP. The reload is asynchronous - the response returns immediately once the reload has been enqueued; the actual reload completes in the background on the main goroutine.
curl -s -X POST http://[::]:9731/api/v1/reload | jq .
Response - HTTP 202
{"status": "reload triggered"}
Returns HTTP 409 if a reload is already queued.
POST /api/v1/reload/sources
Re-fetches all external prefix sources (IRR, NetBox, URL subnets, whitelists) and rebuilds the group LPM trie and whitelist manager — without touching BGP peers, escalation config, or notification targets. Useful when upstream prefix data has changed and you want the update applied immediately rather than waiting for the 12-hour background refresh.
The reload is asynchronous — the response returns immediately once the request has been enqueued.
curl -s -X POST http://[::]:9731/api/v1/reload/sources | jq .
Response - HTTP 202
{"status": "sources refresh triggered"}
Returns HTTP 409 if a sources refresh is already in progress.
What changes vs. what stays the same:
| Component | Updated |
|---|---|
| Group LPM trie (IP → group mapping) | yes |
| Whitelist manager | yes |
| IRR / NetBox / URL-subnet prefix metrics | yes |
| BGP peers | no |
| Escalation config and thresholds | no |
| Notification targets | no |
| Active mitigations | no |
Use POST /api/v1/reload if you also need to apply changes to the YAML config file.
GET /api/v1/config
Returns a sanitized summary of the active configuration - useful for verifying what is loaded without reading the config file directly. Sensitive fields (NetBox tokens, BGP passwords) are never included.
curl -s http://[::]:9731/api/v1/config | jq .
Response
{
"groups": [
{
"name": "customer-a",
"subnets": ["198.51.100.0/24"],
"escalation_levels": 2,
"rules_count": 1,
"has_irr": true,
"has_netbox": false,
"rules": [
{
"name": "udp-amplification",
"protocol": 17,
"dst_ports": [53, 123],
"src_ports": [],
"icmp_types": [],
"icmp_codes": [],
"tcp_flags": ["SYN"],
"fragments": false,
"escalation_levels": [
{"level": 1, "mitigation_type": "flowspec", "bps": 500000000}
]
}
],
"whitelist": {
"subnet_count": 12,
"irr_as_sets": ["AS-CUSTOMERA"]
}
}
],
"netflow": {
"listen_addr": "[::]:2055",
"aggregation_window": "10s",
"default_sampling_rate": 1000
},
"sflow": {"enabled": false},
"http": {"listen_addr": "[::]:9731"},
"bgp_peers": [
{"address": "192.0.2.1", "remote_as": 65001}
],
"defaults": {
"hold_time": "15m0s",
"max_concurrent_mitigations": 0,
"flowspec_max_rules": 50,
"escalation_levels": [
{"level": 1, "mitigation_type": "flowspec", "bps": 500000000},
{"level": 2, "mitigation_type": "blackhole", "bps": 2000000000}
]
},
"log_level": "info",
"pcap": {
"enabled": true,
"dir": "/var/lib/flowwler/pcaps"
},
"storage": {
"enabled": true,
"path": "/var/lib/flowwler/flowwler.db",
"retention_days": 90
}
}
| Field | Description |
|---|---|
groups[].escalation_levels |
Number of group-level escalation levels (0 when the group inherits defaults.escalation_levels) |
groups[].rules |
Named rules for the group, with match criteria and per-rule escalation levels. Same shape as GET /api/v1/groups/{group}/rules, minus the live total_matches/active_victims fields. |
groups[].whitelist |
Group-level whitelist override. Omitted when the group has none (falls back to defaults.whitelist) |
sflow.listen_addr |
Omitted when sFlow is disabled |
defaults.flowspec_max_rules |
Rule budget before FlowSpec consolidation kicks in (0 = unlimited) |
defaults.escalation_levels |
Global escalation levels, used by any group that defines none of its own |
defaults.whitelist |
Global whitelist. Omitted when none is configured |
pcap.dir |
Omitted when PCAP capture is disabled |
storage.enabled |
false when storage.path is unset in config (attack history persistence is off) |
storage.path, storage.retention_days |
Omitted when storage is disabled |
whitelist object fields (groups[].whitelist and defaults.whitelist):
| Field | Description |
|---|---|
subnet_count |
Number of CIDR prefixes currently loaded for this scope (after IRR/URL resolution) |
irr_as_sets |
AS-SETs configured for this scope. Omitted when none |
netbox_urls |
NetBox source URLs configured for this scope. Omitted when none |
url_sources |
Number of plain URL prefix-list sources configured. Omitted when zero |
GET /api/v1/mitigations
Returns all currently active mitigations - both those triggered automatically by the escalation engine and those created manually via the API. To filter by a specific victim IP, append it to the path: GET /api/v1/mitigations/{ip}.
Response
[
{
"victim_ip": "198.51.100.1",
"group_name": "customer-a",
"type": "blackhole",
"source": "auto",
"activated_at": "2026-03-25T10:05:00Z",
"uuid_count": 1
},
{
"victim_ip": "203.0.113.5",
"group_name": "manual",
"type": "flowspec",
"source": "manual",
"activated_at": "2026-03-25T10:10:00Z",
"uuid": "a3f1c2d4e5b60718"
},
{
"victim_ip": "203.0.113.0",
"group_name": "manual",
"type": "subnet-blackhole",
"source": "manual",
"activated_at": "2026-03-25T10:15:00Z",
"uuid_count": 1,
"prefix": "203.0.113.0/24"
}
]
| Field | Description |
|---|---|
victim_ip |
Protected IP address |
group_name |
Group that triggered the mitigation, or "manual" for API-created mitigations |
type |
blackhole, subnet-blackhole, or flowspec |
source |
auto (escalation engine) or manual (API) |
attack_id |
Links this mitigation to its record in GET /api/v1/attacks. Omitted for alert-only escalations. |
activated_at |
RFC 3339 timestamp when the mitigation was activated |
uuid_count |
Number of announced BGP paths (blackhole/subnet-blackhole) |
uuid |
UUID hex of the announced path (manual FlowSpec only) |
prefix |
Actually-announced CIDR for subnet-blackhole mitigations (e.g. 198.51.100.0/24). Omitted for blackhole/flowspec |
POST /api/v1/mitigations
Manually activates a mitigation immediately, without waiting for threshold detection.
If a notification manager is configured, an activated event is sent to all matching targets (group_name is "manual" in the event payload).
Blackhole
Announces a /32 (IPv4) or /128 (IPv6) blackhole route.
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{
"type": "blackhole",
"victim_ip": "198.51.100.1",
"community": "65535:666",
"next_hop": "192.0.2.1"
}' | jq .
Subnet blackhole
Announces a /24 (IPv4) or /48 (IPv6) blackhole for the subnet containing the victim IP by default. Pass an explicit CIDR in victim_ip to announce that exact prefix instead (e.g. to match a /44 or /36 aggregate) — this is the only way to get a non-default prefix length from this endpoint. A group's ipv4_prefix_len/ipv6_prefix_len (configured on its subnet-blackhole: config block — see Configuration) only changes the default used by that group's automatic, escalation-triggered subnet-blackholes; it has no effect on manual API requests, which always fall back to the built-in /24//48 when victim_ip is a bare IP.
# Auto-derived /24 from a bare IP
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{
"type": "subnet-blackhole",
"victim_ip": "198.51.100.1",
"community": "65535:666",
"next_hop": "192.0.2.1"
}' | jq .
# Explicit prefix, e.g. to match a customer's /44 IPv6 aggregate
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{
"type": "subnet-blackhole",
"victim_ip": "2001:db8::/44",
"community": "65535:666"
}' | jq .
FlowSpec
Announces a FlowSpec rule. Each call announces exactly one rule and returns its UUID. Call multiple times to match different sources or ports.
curl -s -X POST http://[::]:9731/api/v1/mitigations \
-H 'Content-Type: application/json' \
-d '{
"type": "flowspec",
"victim_ip": "198.51.100.1",
"action": "discard",
"community": "65000:100:200",
"match": {
"protocol": 17,
"dst_ports": [53, 123]
}
}' | jq .
All match fields are optional. Omitted or zero fields match any value.
Request fields
| Field | Type | Description |
|---|---|---|
type |
string | blackhole, subnet-blackhole, or flowspec |
victim_ip |
string | Destination IP to protect. Bare IP for blackhole/flowspec (a CIDR's host bits are silently masked if given). For subnet-blackhole, a bare IP always auto-derives the built-in /24 (IPv4) / /48 (IPv6) default — a group's configured ipv4_prefix_len/ipv6_prefix_len applies only to that group's automatic, escalation-triggered subnet-blackholes, not manual requests; an explicit CIDR (e.g. 203.0.113.0/24) is announced verbatim and is the only way to get a non-default prefix length here. |
community |
string | BGP community to attach. Standard format: AS:VALUE (RFC 1997, 16-bit parts). Large format: ASN:Local1:Local2 (RFC 8092, 32-bit parts). Optional for flowspec (no default); defaults to 65535:666 for blackhole/subnet-blackhole when omitted. |
action |
string | FlowSpec only: discard (default), rate-limit, or redirect |
rate_limit_bps |
number | FlowSpec rate-limit in bits/sec. 0 = discard |
next_hop |
string | FlowSpec redirect only: scrubbing-center next-hop IP (RFC 7674). May be combined with vrf. For blackhole/subnet-blackhole: BGP next-hop (default 192.0.2.1 / 100::1). |
vrf |
string | FlowSpec redirect only: Route Target in "ASN:LocalAdmin" notation (RFC 5575 §7). May be combined with next_hop. |
match.src_ip |
string | Source IP to match (FlowSpec) |
match.protocol |
number | IP protocol number (6=TCP, 17=UDP, …) |
match.dst_ports |
array of numbers | Destination ports. Empty/omitted = any. A single-element list may carry an explicit 0 (e.g. an hping3 --destport 0 flood) — that is a real match criterion, not the same as "any". More than one element is an OR'd match list. |
match.src_ports |
array of numbers | Source ports (empty/omitted = any) |
match.tcp_flags |
array of strings | TCP flags to match, e.g. ["SYN","ACK"] (valid: FIN SYN RST PSH ACK URG ECE CWR). Only meaningful for protocol: 6 (TCP). |
match.fragments |
boolean | When true, matches only fragmented packets (FlowSpec type-12 is-fragment) |
match.icmp_types |
array of numbers | ICMP types to match (0-255), e.g. [8] for echo request. Only meaningful for protocol: 1 (ICMP). |
match.icmp_codes |
array of numbers | ICMP codes to match (0-255) |
match.min_pkt_len |
number | Minimum packet length (bytes) |
match.max_pkt_len |
number | Maximum packet length (bytes) |
Response
Blackhole / subnet-blackhole - HTTP 201:
{
"victim_ip": "198.51.100.1",
"type": "blackhole",
"uuid": "a3f1c2d4e5b60718"
}
FlowSpec - HTTP 201:
{
"uuid": "a3f1c2d4e5b60718",
"suggested_flowspec": {
"ranked_by": "bps",
"unique_source_count": 3,
"spoofed_sources": false,
"rules": [
{"src_ip": "203.0.113.9", "protocol": 17, "dst_ports": [80], "coverage_pct": 80},
{"src_ip": "203.0.113.10", "protocol": 17, "dst_ports": [80], "coverage_pct": 15}
]
}
}
suggested_flowspec is included when the engine's most recent snapshot contains traffic data for the victim IP and a dominant pattern is detectable (see FlowSpec suggestions). It is omitted when no snapshot data is available yet or the attack traffic is too distributed. Save the uuid to withdraw a specific FlowSpec rule later with DELETE /api/v1/mitigations/uuid/{uuid}.
DELETE /api/v1/mitigations/ip/{ip}
Withdraws every active mitigation for a victim IP in one call - blackhole, subnet-blackhole, and all FlowSpec rules - regardless of how they were activated (manual or automatic). Fires a cleared notification for each withdrawn manual mitigation, pairing it with the original attack_id.
Automatic mitigations (triggered by the escalation engine) are immediately withdrawn from BGP and their escalation state is transitioned to hold-down. The hold-down timer runs as normal; if the attack traffic is still above threshold when it expires, the engine will re-escalate.
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/ip/198.51.100.1 | jq .
Response - HTTP 200
{
"status": "withdrawn",
"victim_ip": "198.51.100.1"
}
DELETE /api/v1/mitigations/{type}/{ip}
Withdraws all active mitigations of the given type for a victim IP - both manual and automatic. Fires a cleared notification for each withdrawn manual mitigation, pairing it with the original attack_id.
- For
blackholeandsubnet-blackhole: withdraws the BGP route and transitions any matching escalation engine state to hold-down. - For
flowspec: withdraws all announced FlowSpec rules for that victim (both manual and auto-managed) and transitions matching escalation engine states to hold-down.
Automatic mitigations transitioned to hold-down will re-escalate when the hold-down timer expires if traffic remains above threshold.
{ip} accepts either a bare IP or a CIDR (e.g. 2001:db8::/44) - use the same CIDR the mitigation was created with when withdrawing a subnet-blackhole that was activated via an explicit victim_ip CIDR (see POST /api/v1/mitigations). Note this CIDR form is not accepted by DELETE /api/v1/mitigations/ip/{ip} below, which only takes a bare IP.
# Remove a manual blackhole
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/blackhole/198.51.100.1 | jq .
# Remove all manual FlowSpec rules for a victim
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/flowspec/198.51.100.1 | jq .
# Remove a subnet-blackhole that was created with an explicit CIDR
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/subnet-blackhole/2001:db8::/44 | jq .
Response - HTTP 200
{
"status": "withdrawn",
"type": "blackhole",
"victim_ip": "198.51.100.1"
}
DELETE /api/v1/mitigations/uuid/{uuid}
Withdraws a mitigation by the UUID returned when it was activated - blackhole, subnet-blackhole, or a FlowSpec rule. Works for both manual and auto-managed mitigations. Behaviour differs by source and type:
- Manual blackhole/subnet-blackhole (
uuidfrom thePOST /api/v1/mitigationscreate response): withdraws that mitigation and fires aclearednotification, identical toDELETE /api/v1/mitigations/{type}/{ip}for the same victim/type. - Manual FlowSpec rule (
"source": "manual"inGET /api/v1/flowspec/rules): withdraws exactly that one rule and fires aclearednotification. - Auto-managed mitigation (
"source": "auto"): withdraws all paths for that victim/mitigation-type (all rules, for FlowSpec) and transitions the escalation engine state to hold-down (fires aholddownevent). The engine will re-escalate if traffic remains above threshold after the hold-down period expires.
curl -s -X DELETE http://[::]:9731/api/v1/mitigations/uuid/a3f1c2d4e5b60718 | jq .
Response - HTTP 200
{
"status": "withdrawn",
"uuid": "a3f1c2d4e5b60718"
}
Returns HTTP 404 if the UUID is not found.
GET /api/v1/mitigations/{ip}
Returns active mitigations for a single victim IP. The response format is identical to GET /api/v1/mitigations. {ip} accepts either a bare IP or a CIDR (e.g. 2001:db8::/44). Returns HTTP 400 if the value is neither a valid IP nor a valid CIDR.
curl -s http://[::]:9731/api/v1/mitigations/198.51.100.1 | jq .
GET /api/v1/escalations
Returns the full internal state of the escalation engine. Useful for debugging threshold tuning or verifying which mitigations are active and at what level.
To filter to a specific victim, use GET /api/v1/escalations/{group}/{ip}. The response format is the same; the array contains only states matching that group and IP. Returns HTTP 400 for an invalid IP.
curl -s http://[::]:9731/api/v1/escalations | jq .
curl -s http://[::]:9731/api/v1/escalations/customer-a/198.51.100.1 | jq .
Response
[
{
"group_name": "customer-a",
"victim_ip": "198.51.100.1",
"subnet_key": "",
"rule_key": "",
"protocol": 0,
"phase": "active",
"current_level": 1,
"mitigation_type": "blackhole",
"last_bps": 2500000000,
"last_pps": 1800000,
"level_entered_at": "2026-03-13T10:00:00Z",
"hold_down_started": "0001-01-01T00:00:00Z",
"hold_down_ends_at": "0001-01-01T00:00:00Z",
"attack_id": "a3f1c2d4e5b607180a1b2c3d4e5f6071",
"attack_started_at": "2026-03-13T10:00:00Z"
}
]
| Field | Description |
|---|---|
group_name |
Config group this victim belongs to |
victim_ip |
Victim IP address. Empty string for subnet-level (carpet bomb) states; see subnet_key |
subnet_key |
CIDR of the subnet for carpet bomb subnet-level states. Omitted for per-IP states, where victim_ip is set instead |
rule_key |
Empty string for group-level escalation; rule name for rule-level |
protocol |
IANA protocol number for subnet-level (carpet bomb) states — victims are clustered by protocol before counting toward min_victims, so a UDP and a TCP carpet bomb in the same subnet are separate states. 0 (omitted) for per-IP states, where this dimension doesn't apply |
phase |
idle, active, or hold-down |
current_level |
Index of the active escalation level (0-based) |
mitigation_type |
Active mitigation type (blackhole, subnet-blackhole, flowspec) |
last_bps |
Most recent smoothed bits/sec observed for this victim |
last_pps |
Most recent smoothed packets/sec |
level_entered_at |
When the current level was entered; resets on each escalation step |
hold_down_started |
When hold-down began (zero if not in hold-down) |
hold_down_ends_at |
When hold-down will expire; present only when phase is hold-down |
attack_id |
Stable hex ID for the current attack session; same value threads through all notifications for the attack. Omitted when phase is idle |
attack_started_at |
When the attack was first detected (Idle→Active transition); unlike level_entered_at this does not change as the attack escalates through levels. Omitted when phase is idle |
GET /api/v1/bgp/peers
Returns the session state of all configured BGP peers.
curl -s http://[::]:9731/api/v1/bgp/peers | jq .
Response
[
{
"NeighborAddress": "192.0.2.1",
"PeerASN": 65001,
"SessionState": "established",
"Established": true,
"ReceivedUpdates": 0,
"SentUpdates": 3
}
]
| Field | Description |
|---|---|
NeighborAddress |
Peer IP address |
PeerASN |
Peer autonomous system number |
SessionState |
FSM state: idle, connect, active, opensent, openconfirm, or established |
Established |
true if the session is fully established |
ReceivedUpdates |
Number of BGP UPDATE messages received from this peer |
SentUpdates |
Number of BGP UPDATE messages sent to this peer |
GET /api/v1/bgp/routes
Returns all routes in the embedded GoBGP global RIB, grouped by address family.
curl -s http://[::]:9731/api/v1/bgp/routes | jq .
Response
{
"ipv4_unicast": [{"prefix": "198.51.100.1/32", "path_count": 1}],
"ipv6_unicast": [],
"ipv4_flowspec": [{"prefix": "198.51.100.1/32", "path_count": 1}],
"ipv6_flowspec": []
}
| Field | Description |
|---|---|
ipv4_unicast |
IPv4 unicast (blackhole/subnet-blackhole) routes |
ipv6_unicast |
IPv6 unicast routes |
ipv4_flowspec |
IPv4 FlowSpec NLRI routes |
ipv6_flowspec |
IPv6 FlowSpec NLRI routes |
Each entry has prefix (destination prefix string) and path_count (number of paths for that prefix). All four arrays are always present; empty arrays indicate no routes in that family.
GET /api/v1/groups
Returns all configured groups with their current escalation status. For a single group, use GET /api/v1/groups/{group}.
curl -s http://[::]:9731/api/v1/groups | jq .
Response
[
{
"name": "customer-a",
"subnets": ["198.51.100.0/24", "2001:db8::/32"],
"escalation_levels": [
{
"level": 1,
"mitigation_type": "flowspec",
"bps": 500000000,
"escalate_after": "5m0s"
},
{
"level": 2,
"mitigation_type": "blackhole",
"bps": 2000000000
}
],
"rules_count": 1,
"inbound_bps": 2500000000,
"inbound_pps": 1800000,
"outbound_bps": 500000000,
"outbound_pps": 400000,
"active_victims": [
{
"victim_ip": "198.51.100.1",
"phase": "active",
"current_level": 0,
"mitigation_type": "blackhole",
"last_bps": 2500000000,
"last_pps": 1800000,
"attack_id": "a3f1c2d4e5b607189c2d1e4f5a6b0718"
}
]
}
]
| Field | Description |
|---|---|
name |
Group name as defined in config |
subnets |
Protected CIDR prefixes (static + those resolved from IRR/NetBox) |
escalation_levels |
Array of escalation level descriptors - either from the group's own config or, when the group has none, inherited from the defaults section |
rules_count |
Number of named rules defined on the group |
inbound_bps |
Current smoothed aggregate inbound bits/sec across all victims in the group |
inbound_pps |
Current smoothed aggregate inbound packets/sec across all victims in the group |
outbound_bps |
Current smoothed aggregate outbound bits/sec (traffic sourced from the group's prefixes) |
outbound_pps |
Current smoothed aggregate outbound packets/sec |
active_victims |
Victim IPs currently in active or hold-down phase at the group level; empty array when none |
Each escalation_levels entry:
| Field | Description |
|---|---|
level |
Level number (1-based) |
mitigation_type |
blackhole, subnet-blackhole, flowspec, or empty string for alert-only |
bps |
BPS threshold that triggers this level; omitted if not set |
pps |
PPS threshold that triggers this level; omitted if not set |
escalate_after |
Duration string before auto-escalation to the next level; omitted if not set |
Each active_victims entry:
| Field | Description |
|---|---|
victim_ip |
Victim IP address |
phase |
active or hold-down |
current_level |
Zero-based index of the active escalation level |
mitigation_type |
Active mitigation type (blackhole, subnet-blackhole, flowspec) |
last_bps |
Most recent smoothed bits/sec for this victim |
last_pps |
Most recent smoothed packets/sec |
attack_id |
Links to the attack record in GET /api/v1/attacks. Omitted for alert-only escalations. |
GET /api/v1/groups/{group}
Returns detail for a single group. The response shape is identical to one element of the GET /api/v1/groups array. Returns HTTP 404 if the group name does not exist.
curl -s http://[::]:9731/api/v1/groups/customer-a | jq .
GET /api/v1/groups/{group}/subnets
Returns the full list of resolved subnets for the group - static prefixes from config merged with any prefixes loaded from IRR or NetBox. Returns HTTP 404 if the group name does not exist.
curl -s http://[::]:9731/api/v1/groups/customer-a/subnets | jq .
Response
[
"198.51.100.0/24",
"2001:db8::/32",
"203.0.113.0/24"
]
GET /api/v1/groups/{group}/rules
Returns all named rules for a group with match criteria and current rule-level escalation status. Returns HTTP 404 if the group name does not exist.
curl -s http://[::]:9731/api/v1/groups/customer-a/rules | jq .
Response
[
{
"name": "udp-amplification",
"protocol": 17,
"dst_ports": [53, 123],
"src_ports": [],
"icmp_types": [],
"icmp_codes": [],
"tcp_flags": ["SYN"],
"fragments": true,
"escalation_levels": 1,
"total_matches": 4821,
"active_victims": [
{
"victim_ip": "198.51.100.1",
"phase": "active",
"current_level": 0,
"mitigation_type": "flowspec",
"last_bps": 500000000,
"last_pps": 120000
}
]
}
]
| Field | Description |
|---|---|
name |
Rule name |
protocol |
IP protocol number matched by this rule (0 = any / not set) |
dst_ports |
Destination port list (empty = any) |
src_ports |
Source port list (empty = any) |
icmp_types |
ICMP types to match (0-255), e.g. [8] for echo request. Empty/omitted = any. Only meaningful for protocol: 1 (ICMP). |
icmp_codes |
ICMP codes to match (0-255). Empty/omitted = any. |
tcp_flags |
TCP flag names that must be set (e.g. ["SYN"], ["SYN","ACK"]). Omitted when not configured. |
fragments |
true when the rule matches fragmented packets only. Omitted when false. |
pkt_len |
Packet length filter. Object with min and max (bytes, 0 = unbounded). Omitted when no filter is set. |
subnet |
Subnet-level carpet bomb detection config. See below. Omitted when not configured. |
escalation_levels |
Number of escalation levels on the rule |
total_matches |
Cumulative number of times this rule has been matched since daemon start |
active_victims |
Victims currently in active or hold-down phase for this rule. For subnet rules, victim_ip contains the aggregated subnet CIDR (e.g. "10.1.2.0/24") rather than a single host address. |
subnet fields:
| Field | Description |
|---|---|
ipv4 |
Prefix length used to bucket IPv4 victims (e.g. 24 → per-/24). 0 = use the group's own subnet size. |
ipv6 |
Prefix length used to bucket IPv6 victims (e.g. 48 → per-/48). 0 = use the group's own subnet size. |
min_victims |
Minimum number of distinct victim IPs that must be targeted within the subnet window before the rule fires. |
GET /api/v1/groups/{group}/targets
Returns the current per-IP inbound and outbound traffic rates for all target IPs in a group that are visible in the most recent aggregator snapshot (approximately 1-second cadence). Unlike active_victims in the groups endpoint, this includes IPs that are below the detection threshold and in idle state. Returns an empty array when no traffic is currently tracked for the group. Returns HTTP 404 if the group name does not exist.
curl -s http://[::]:9731/api/v1/groups/customer-a/targets | jq .
Response
[
{
"ip": "198.51.100.1",
"inbound_bps": 1234567,
"inbound_pps": 4200,
"discard_bps": 0,
"discard_pps": 0,
"outbound_bps": 890123,
"outbound_pps": 2100
},
{
"ip": "198.51.100.2",
"inbound_bps": 55000,
"inbound_pps": 120,
"discard_bps": 0,
"discard_pps": 0,
"outbound_bps": 40000,
"outbound_pps": 95
}
]
| Field | Description |
|---|---|
ip |
Target IP address |
inbound_bps |
Current smoothed inbound traffic rate in bits per second |
inbound_pps |
Current smoothed inbound traffic rate in packets per second |
discard_bps |
Portion of inbound traffic already being dropped by routers (bits per second) |
discard_pps |
Portion of inbound traffic already being dropped by routers (packets per second) |
outbound_bps |
Current smoothed outbound traffic rate in bits per second |
outbound_pps |
Current smoothed outbound traffic rate in packets per second |
GET /api/v1/flowspec/rules
Returns all currently active FlowSpec rules - both those announced automatically by the escalation engine and those created manually via the API. Each element represents one victim IP and lists every individual FlowSpec NLRI announced for it.
curl -s http://[::]:9731/api/v1/flowspec/rules | jq .
Response
[
{
"victim_ip": "198.51.100.1",
"group_name": "customer-a",
"source": "auto",
"rules": [
{
"uuid": "a3f1c2d4e5b60718",
"victim_ip": "198.51.100.1",
"src_ip": "10.0.0.1",
"protocol": 17,
"dst_ports": [53],
"action": "discard"
},
{
"uuid": "b9e4a1c7d2f38024",
"victim_ip": "198.51.100.1",
"src_ip": "10.0.0.2",
"protocol": 17,
"dst_ports": [53],
"action": "discard"
}
]
},
{
"victim_ip": "203.0.113.5",
"group_name": "manual",
"source": "manual",
"rules": [
{
"uuid": "c1d2e3f4a5b60718",
"victim_ip": "203.0.113.5",
"src_ip": "10.1.2.3",
"protocol": 17,
"dst_ports": [53],
"action": "discard"
}
]
},
{
"victim_ip": "198.51.100.7",
"group_name": "customer-b",
"source": "auto",
"rules": [
{
"uuid": "d2e3f4a5b6c70819",
"victim_ip": "198.51.100.7",
"protocol": 1,
"icmp_types": [8],
"icmp_codes": [0],
"action": "discard"
}
]
}
]
| Field | Description |
|---|---|
victim_ip |
Protected destination IP |
group_name |
Group that triggered the mitigation, or "manual" for API-created rules |
source |
auto (escalation engine) or manual (API) |
rules |
List of announced FlowSpec NLRIs for this victim |
Per-rule fields:
| Field | Description |
|---|---|
uuid |
BGP path UUID (hex); use with DELETE /api/v1/mitigations/uuid/{uuid} to withdraw |
victim_ip |
Destination IP matched by this rule |
src_ip |
Source IP matched by this rule (omitted if destination-only) |
protocol |
IP protocol number (0 / omitted = any) |
dst_ports |
Destination ports (empty/omitted = any). A single-element list may carry an explicit 0 (e.g. an hping3 --destport 0 flood) — that is a real match criterion, not the same as "any". More than one element is an OR'd match list. |
src_ports |
Source ports (empty/omitted = any) |
icmp_types |
Observed ICMP types (e.g. 8 = echo request), automatically matched for ICMP-protocol mitigations — no config needed. Empty/omitted if the traffic isn't ICMP or no type data was observed. |
icmp_codes |
Observed ICMP codes, same automatic matching as icmp_types. |
min_pkt_len |
Minimum packet length in bytes (0 / omitted = any) |
max_pkt_len |
Maximum packet length in bytes (0 / omitted = any) |
pkt_len_approximate |
Always false/omitted for a live FlowSpec route. A route's min_pkt_len/max_pkt_len, when present, is always native exporter data or an explicit pkt_len match config — a purely derived (approximate) value is never attached to an announced route's NLRI. The equivalent field on a FlowSpec suggestion (GET suggestions payload) can be true, since a suggestion is reviewed by an operator before ever being applied. |
tcp_flags_bitmask |
TCP flags bitmask matched by this rule (0 / omitted = any; e.g. 2 = SYN). Either the operator's static match.tcp_flags config, or — when no static config is set — the value automatically observed in traffic (see internal/mitigation/flowspec.go's resolveTCPFlags). Only meaningful for protocol: 6 (TCP) rules. A raw bitmask, distinct from the tcp_flags flag-name list ([]string, e.g. ["SYN","ACK"]) used on rule-config responses (GET /api/v1/groups/{group}/rules and similar). |
fragments |
true when this rule matches only fragmented packets (FlowSpec type-12 is-fragment); false/omitted otherwise |
action |
discard, rate-limit, or redirect |
redirect_next_hop |
Scrubbing-center IP for redirect rules (omitted otherwise) |
redirect_vrf |
VRF Route Target in "ASN:LocalAdmin" notation for redirect rules (omitted otherwise) |
Returns an empty array when no FlowSpec mitigations are currently active.
GET /api/v1/notifications/targets
Returns a health snapshot for every configured notification target. Useful for verifying that backends are reachable and that recent notifications were delivered.
curl -s http://[::]:9731/api/v1/notifications/targets | jq .
Response
[
{
"name": "ops-pagerduty",
"type": "pagerduty",
"scope": "global",
"last_fired_at": "2026-03-29T10:05:02Z",
"last_succeeded_at": "2026-03-29T10:05:02Z"
},
{
"name": "customer-a-webhook",
"type": "webhook",
"scope": "customer-a",
"last_fired_at": "2026-03-29T09:58:11Z",
"last_succeeded_at": "2026-03-29T08:00:00Z",
"last_error": "Post \"https://...\": dial tcp: connection refused"
}
]
| Field | Description |
|---|---|
name |
Target name as defined in config |
type |
Backend type: webhook, slack, teams, telegram, pagerduty, pushover, alertmanager, jira, zammad |
scope |
"global" for targets under the top-level notifications: block; group name for per-group targets |
last_fired_at |
RFC 3339 timestamp of the most recent send attempt; omitted if no attempt has been made since daemon start |
last_succeeded_at |
RFC 3339 timestamp of the most recent successful delivery; omitted if no delivery has succeeded since daemon start |
last_error |
Error message from the most recent failed attempt; omitted when the last attempt succeeded |
Target health state resets on config reload (SIGHUP). Returns an empty array when no notification targets are configured.
POST /api/v1/notifications/targets/{name}/test
Sends a synthetic test notification to the named target, bypassing its event-type filter. Useful for verifying that a backend is reachable and correctly configured without waiting for a real attack.
The health state (last_fired_at, last_succeeded_at, last_error) for the target is updated so the result is visible in GET /api/v1/notifications/targets.
# Send a test notification with default synthetic data
curl -s -X POST http://[::]:9731/api/v1/notifications/targets/ops-pagerduty/test | jq .
# Override victim IP and group name in the test event
curl -s -X POST http://[::]:9731/api/v1/notifications/targets/ops-pagerduty/test \
-H 'Content-Type: application/json' \
-d '{"victim_ip": "198.51.100.1", "group_name": "transit"}' | jq .
Request body (optional)
| Field | Type | Default | Description |
|---|---|---|---|
victim_ip |
string | 192.0.2.1 |
Victim IP in the test event. Must be a valid IPv4 or IPv6 address. |
group_name |
string | test |
Group name in the test event. Allowed characters: letters, digits, -, _. |
If the body is omitted entirely the defaults are used.
Response - HTTP 200
{
"status": "sent",
"target": "ops-pagerduty"
}
Error responses
| Code | Meaning |
|---|---|
400 |
victim_ip is not a valid IP address, or group_name contains disallowed characters |
404 |
No target with the given name exists |
502 |
Target was found but the backend returned an error (e.g. invalid token, connection refused) |
GET /api/v1/attacks
Returns a list of attack sessions from the SQLite history database, newest first.
curl -s http://[::]:9731/api/v1/attacks | jq .
curl -s 'http://[::]:9731/api/v1/attacks?victim_ip=198.51.100.1&active=true' | jq .
Query parameters
| Parameter | Description |
|---|---|
group |
Filter by group name. Omit for all groups. |
victim_ip |
Filter by victim IP address. Omit for all IPs. |
active |
Set to true to return only attacks without a cleared timestamp (currently active). |
limit |
Maximum number of results. Default: 100. |
offset |
Number of results to skip. Used with limit for pagination. Default: 0. |
Response
[
{
"attack_id": "a3f1c2d4e5b607189c2d1e4f5a6b0718",
"group_name": "transit",
"rule_name": "udp-amp",
"victim_ip": "198.51.100.1",
"started_at": "2026-03-30T10:00:00Z",
"cleared_at": "2026-03-30T10:18:00Z",
"peak_bps": 4500000000,
"peak_pps": 6000000,
"peak_level": 2,
"mitigation_type": "flowspec"
},
{
"attack_id": "b9e4a1c7d2f380249c3e2d5f6a7b1829",
"group_name": "customers",
"victim_ip": "203.0.113.5",
"started_at": "2026-03-30T11:30:00Z",
"peak_bps": 800000000,
"peak_pps": 900000,
"peak_level": 1,
"mitigation_type": "blackhole",
"notification_only": false
}
]
| Field | Description |
|---|---|
attack_id |
Stable hex ID for this attack session. Matches the attack_id in notification payloads. |
group_name |
Group whose threshold triggered the attack detection. |
rule_name |
Rule that triggered detection. Omitted for group-level (non-rule) detections. |
victim_ip |
Destination IP under attack. |
started_at |
RFC 3339 timestamp of the Idle→Active transition. |
cleared_at |
RFC 3339 timestamp when the attack was cleared. Omitted for active attacks. |
peak_bps |
Highest BPS observed during this attack session. |
peak_pps |
Highest PPS observed during this attack session. |
peak_level |
Highest escalation level reached. |
mitigation_type |
BGP mitigation type: blackhole, subnet-blackhole, or flowspec. Omitted for alert-only attacks. |
notification_only |
true when the attack triggered notifications only (no BGP mitigation). Omitted when false. |
pcap_file |
Filename of the associated PCAP capture in the configured PCAP directory. Omitted when no capture was taken. |
Returns 501 Not Implemented when storage is disabled in config.
GET /api/v1/attacks/{attack_id}
Returns the full detail for a single attack session, including all state-transition events.
curl -s http://[::]:9731/api/v1/attacks/a3f1c2d4e5b607189c2d1e4f5a6b0718 | jq .
Response
{
"attack_id": "a3f1c2d4e5b607189c2d1e4f5a6b0718",
"group_name": "transit",
"rule_name": "udp-amp",
"victim_ip": "198.51.100.1",
"started_at": "2026-03-30T10:00:00Z",
"cleared_at": "2026-03-30T10:18:00Z",
"peak_bps": 4500000000,
"peak_pps": 6000000,
"peak_level": 2,
"mitigation_type": "flowspec",
"events": [
{
"event_type": "activated",
"timestamp": "2026-03-30T10:00:00Z",
"level": 1,
"bps": 1200000000,
"pps": 1500000,
"threshold_bps": 1000000000
},
{
"event_type": "escalated",
"timestamp": "2026-03-30T10:02:30Z",
"level": 2,
"prev_level": 1,
"bps": 4500000000,
"pps": 6000000,
"threshold_bps": 4000000000
},
{
"event_type": "holddown",
"timestamp": "2026-03-30T10:05:00Z",
"level": 2,
"bps": 900000000,
"pps": 1100000,
"discard_bps": 3600000000
},
{
"event_type": "cleared",
"timestamp": "2026-03-30T10:18:00Z",
"level": 2,
"bps": 200000000,
"pps": 250000
}
]
}
The summary fields are identical to GET /api/v1/attacks. The events array contains one entry per state transition, ordered chronologically.
| Event field | Description |
|---|---|
event_type |
activated, escalated, holddown, or cleared |
timestamp |
RFC 3339 timestamp of this state transition |
level |
Escalation level at the time of this event |
prev_level |
Previous escalation level. Omitted for activated events. |
bps |
Smoothed BPS rate at the time of this event |
pps |
Smoothed PPS rate at the time of this event |
discard_bps |
BPS being dropped by the router at this event (non-zero in HoldDown). Omitted when zero. |
threshold_bps |
BPS threshold that triggered or defined this level. Omitted when not set. |
threshold_pps |
PPS threshold that triggered or defined this level. Omitted when not set. |
flowspec_rules |
For FlowSpec mitigations: array of announced NLRI rules. Omitted for other mitigation types. |
suggested_flowspec |
FlowSpec rule suggestions derived from flow data at the time of the event. Present when the mitigation type is not FlowSpec and a dominant traffic pattern was detectable. See the notifications schema for field definitions. |
Returns 404 Not Found when the attack_id does not exist in the database. Returns 501 Not Implemented when storage is disabled in config.
GET /api/v1/attacks/stats
Returns aggregate statistics across all stored attacks. Useful for dashboards and operational reports.
curl -s http://[::]:9731/api/v1/attacks/stats | jq .
Response
{
"total_count": 1284,
"active_count": 3,
"mean_duration_seconds": 732.4,
"peak_bps_ever": 48000000000,
"peak_pps_ever": 12000000,
"top_victims": [
{"name": "198.51.100.1", "attack_count": 42},
{"name": "203.0.113.5", "attack_count": 18}
],
"top_groups": [
{"name": "transit", "attack_count": 800},
{"name": "customer-a", "attack_count": 210}
]
}
| Field | Description |
|---|---|
total_count |
Total number of attack records in the database |
active_count |
Attacks without a cleared timestamp (currently ongoing) |
mean_duration_seconds |
Average duration of cleared attacks in seconds; 0 when no cleared attacks exist |
peak_bps_ever |
Highest single-attack peak BPS ever recorded |
peak_pps_ever |
Highest single-attack peak PPS ever recorded |
top_victims |
Up to 10 most-attacked IPs by attack count, descending |
top_groups |
Up to 10 most-targeted groups by attack count, descending |
Returns 501 Not Implemented when storage is disabled in config.
GET /api/v1/ip/{ip}
Resolves an IP address against the configured group trie and returns the matching group with its escalation configuration. Useful for quickly checking whether an IP is monitored and which policy applies.
curl -s http://[::]:9731/api/v1/ip/198.51.100.5 | jq .
Response - HTTP 200
{
"ip": "198.51.100.5",
"group_name": "customer-a",
"matched_subnet": "198.51.100.0/24",
"subnets": ["198.51.100.0/24", "2001:db8::/32"],
"escalation_levels": [
{"level": 1, "mitigation_type": "flowspec", "bps": 500000000},
{"level": 2, "mitigation_type": "blackhole", "bps": 2000000000}
],
"rules_count": 1
}
| Field | Description |
|---|---|
ip |
Normalized form of the queried IP |
group_name |
Name of the group whose subnets contain this IP |
matched_subnet |
The most-specific subnet in the group that contains the IP |
subnets |
All subnets configured for this group (static + IRR/NetBox) |
escalation_levels |
Escalation levels in effect (group's own, or inherited from defaults) |
rules_count |
Number of named rules defined on the group |
Returns 404 Not Found when the IP does not fall within any configured group's subnets.
GET /api/v1/flowspec/suggest/{ip}
Returns live FlowSpec rule suggestions for a victim IP derived from the most recent aggregator snapshot. This is the same analysis used internally when activating mitigations - querying it standalone lets operators assess the attack traffic pattern before deciding what to do.
# Rank sources by BPS (default)
curl -s http://[::]:9731/api/v1/flowspec/suggest/198.51.100.1 | jq .
# Rank sources by PPS instead
curl -s 'http://[::]:9731/api/v1/flowspec/suggest/198.51.100.1?rank_by=pps' | jq .
Query parameters
| Parameter | Description |
|---|---|
rank_by |
Ranking metric: bps (default) or pps |
Response - HTTP 200
{
"ranked_by": "bps",
"unique_source_count": 4,
"spoofed_sources": false,
"rules": [
{
"src_ip": "10.0.0.1",
"protocol": 17,
"dst_ports": [53],
"coverage_pct": 62
},
{
"src_ip": "10.0.0.2",
"protocol": 17,
"dst_ports": [53],
"coverage_pct": 23
}
]
}
Suggestion rules carry dst_ports/src_ports as arrays (empty/omitted = any; a single explicit 0 is a real match, not "any"), the same shape as live FlowSpec rules above. Unlike live rules, suggestions do not currently surface icmp_types/icmp_codes — the suggestion engine (internal/escalation/suggest.go) doesn't yet compute ICMP dimensions, even when the underlying traffic is ICMP.
See notifications schema for full field definitions.
Returns 404 Not Found when no flow data is available for the requested IP (no recent snapshot, or IP has no traffic).
POST /api/v1/escalations/{group}/{ip}/clear
Forces all escalation states for a victim IP within the given group immediately to idle - withdrawing any active BGP mitigations and firing cleared notifications. This skips the hold-down timer entirely.
Use this when you know traffic has stopped and do not want to wait for the hold-down period to expire. If traffic resumes above threshold the escalation engine will re-escalate normally on the next snapshot tick.
curl -s -X POST http://[::]:9731/api/v1/escalations/customer-a/198.51.100.1/clear | jq .
Response - HTTP 200
{
"status": "cleared",
"group_name": "customer-a",
"victim_ip": "198.51.100.1",
"states_transitioned": 2
}
| Field | Description |
|---|---|
status |
Always "cleared" on success |
group_name |
Group name from the request path |
victim_ip |
Normalized victim IP |
states_transitioned |
Number of escalation states moved to idle (0 if the victim was already idle) |
GET /api/v1/whitelist
Returns a list of all configured whitelist scopes and the number of prefixes loaded in each. Returns HTTP 501 when no whitelist is configured.
curl -s http://[::]:9731/api/v1/whitelist | jq .
Response
[
{
"scope": "defaults",
"prefix_count": 42
},
{
"scope": "customer-a",
"prefix_count": 8,
"is_group_override": true
}
]
| Field | Description |
|---|---|
scope |
"defaults" for the global whitelist; group name for per-group overrides. |
prefix_count |
Number of CIDR prefixes currently loaded in this scope (after IRR/URL resolution). |
is_group_override |
Present and true for per-group scopes that override the global whitelist. Omitted for the "defaults" scope. |
GET /api/v1/whitelist/prefixes
Returns all CIDR prefixes for every configured whitelist scope. Returns HTTP 501 when no whitelist is configured.
curl -s http://[::]:9731/api/v1/whitelist/prefixes | jq .
Response
[
{
"scope": "defaults",
"prefixes": ["10.0.0.0/8", "192.168.0.0/16", "2001:db8::/32"]
},
{
"scope": "customer-a",
"prefixes": ["203.0.113.0/24"],
"is_group_override": true
}
]
| Field | Description |
|---|---|
scope |
"defaults" for the global whitelist; group name for per-group overrides. |
prefixes |
All CIDR prefixes loaded in this scope, in the order they were resolved. |
is_group_override |
Present and true for per-group scopes. Omitted for "defaults". |
GET /api/v1/pcap/captures
Returns a list of currently open per-attack PCAP capture sessions. Returns an empty array when no captures are active. Returns HTTP 501 when pcap.enabled is false in the configuration.
curl -s http://[::]:9731/api/v1/pcap/captures | jq .
Response
[
{
"victim_ip": "198.51.100.1",
"attack_id": "a3f2b1c0d4e5f678"
}
]
| Field | Description |
|---|---|
victim_ip |
The victim IP address for this capture session. |
attack_id |
The internal attack ID (hex string), matches attack_id in escalation state and attack history endpoints. |
GET /api/v1/pcap/attacks
Returns attacks that have an associated PCAP capture, enriched with file-system metadata and a packet preview. Requires both pcap.enabled and storage to be configured; returns HTTP 501 otherwise.
Supports the same query parameters as GET /api/v1/attacks: group, victim_ip, active, limit, offset.
curl -s 'http://[::]:9731/api/v1/pcap/attacks?limit=5' | jq .
Response
[
{
"attack_id": "a3f2b1c0d4e5f67890ab12cd34ef5678",
"group_name": "upstreams",
"victim_ip": "198.51.100.1",
"started_at": "2026-04-20T14:05:30Z",
"cleared_at": "2026-04-20T14:09:12Z",
"peak_bps": 4200000000,
"peak_pps": 1800000,
"peak_level": 2,
"pcap_file": "2026-04-20T14-05-30_198.51.100.1_a3f2b1c0d4e5f67890ab12cd34ef5678.pcap",
"pcap_exists": true,
"pcap_size_bytes": 24576,
"pcap_packet_count": 312,
"pcap_preview": [
{
"timestamp": "2026-04-20T14:05:30.000000Z",
"src_ip": "1.2.3.4",
"dst_ip": "198.51.100.1",
"protocol": 17,
"src_port": 54321,
"dst_port": 80,
"length": 1400
}
]
}
]
| Field | Description |
|---|---|
pcap_file |
Filename of the capture in the configured PCAP directory. |
pcap_exists |
true when the file is still present on disk. |
pcap_size_bytes |
File size in bytes; omitted or 0 when file is missing. |
pcap_packet_count |
Total synthesized packet records in the file; omitted or 0 when file is missing. |
pcap_preview |
Up to 10 decoded packets from the start of the file; omitted when file is missing or empty. |
GET /api/v1/pcap/attacks/{attack_id}
Downloads the PCAP file for a specific attack. Requires both PCAP and storage to be configured. Returns HTTP 404 when the attack has no capture or the file no longer exists on disk.
curl -s -O http://[::]:9731/api/v1/pcap/attacks/a3f2b1c0d4e5f67890ab12cd34ef5678
# Open directly in Wireshark (requires auth header if configured)
wireshark 'http://[::]:9731/api/v1/pcap/attacks/a3f2b1c0d4e5f67890ab12cd34ef5678'
Each record in the PCAP is a synthesized packet derived from a NetFlow/sFlow flow record — an IP header plus transport header (TCP/UDP/ICMP) with no payload. Packet length fields reflect the flow's minimum observed packet size so tools show truncation correctly.
POST /api/v1/pcap/manual
Starts an ad-hoc PCAP capture for any victim IP, independent of any ongoing attack. The capture writes all flows destined for victim_ip until the duration expires or the capture is stopped. Returns HTTP 409 when a manual capture for that IP is already active.
Maximum duration: 5 minutes. Default when duration is omitted: 60 seconds.
curl -s -X POST http://[::]:9731/api/v1/pcap/manual \
-H 'Content-Type: application/json' \
-d '{"victim_ip": "198.51.100.1", "duration": "2m"}'
Request body
| Field | Type | Description |
|---|---|---|
victim_ip |
string | Required. The IP address to capture flows for. |
duration |
string | Go duration string (e.g. "30s", "2m"). Defaults to 60s; capped at 5m. |
Response (HTTP 201)
{
"id": "a1b2c3d4e5f6a7b8c9d0e1f2",
"victim_ip": "198.51.100.1",
"filename": "manual_2026-04-20T14-05-30_198.51.100.1_a1b2c3d4e5f6a7b8c9d0e1f2.pcap",
"started_at": "2026-04-20T14:05:30Z",
"duration": "2m0s",
"active": true
}
GET /api/v1/pcap/manual
Returns all currently active manual captures.
curl -s http://[::]:9731/api/v1/pcap/manual | jq .
GET /api/v1/pcap/manual/{id}
Downloads the PCAP file for a manual capture by its ID. Works for both active and completed captures as long as the file exists on disk.
curl -s -O http://[::]:9731/api/v1/pcap/manual/a1b2c3d4e5f6a7b8c9d0e1f2
Returns HTTP 404 when the capture ID is not known or the file no longer exists on disk.
DELETE /api/v1/pcap/manual/{id}
Stops a manual capture early and returns the capture info. The PCAP file remains on disk and can still be downloaded.
curl -s -X DELETE http://[::]:9731/api/v1/pcap/manual/a1b2c3d4e5f6a7b8c9d0e1f2 | jq .
Returns {"active": false, ...} on success. Returns HTTP 404 when the capture is not found or has already expired.