Skip to content

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 blackhole and subnet-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 (uuid from the POST /api/v1/mitigations create response): withdraws that mitigation and fires a cleared notification, identical to DELETE /api/v1/mitigations/{type}/{ip} for the same victim/type.
  • Manual FlowSpec rule ("source": "manual" in GET /api/v1/flowspec/rules): withdraws exactly that one rule and fires a cleared notification.
  • 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 a holddown event). 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.