GET /bgp/history/as-flows

The movers diff between two instants from and to: how each identity's first external (upstream) AS changed, as mutually-exclusive per-category totals plus the by-upstream-pair movers list.

Auth: all authenticated roles

Query params: as_id (required); from/to (required, RFC 3339); optional routing_domain_id, device_id, af, cap (max movers rows, default 500, max 5000).

Response: 200 OK

{
  "movers": [
    { "category": "upstream_change", "from_as": 3356, "to_as": 1299, "count": 214,
      "sample_prefixes": ["192.0.2.0/24", "203.0.113.0/24"] },
    { "category": "withdraw", "from_as": 3356, "to_as": 0, "count": 9, "sample_prefixes": ["203.0.113.128/25"] },
    { "category": "announce", "from_as": 0, "to_as": 1299, "count": 9, "sample_prefixes": ["198.51.100.64/26"] }
  ],
  "totals": { "upstream_change": 214, "path_internal": 3, "announce": 9, "withdraw": 9, "unchanged": 615 },
  "truncated": false
}

The five categories partition the identities in scope (Σ totals = total identities — mutual exclusivity): upstream_change (first AS differs), path_internal (same first AS, different path/attrs), announce (∅→AS), withdraw (AS→∅), unchanged. A from_as/to_as of 0 means ∅ (no path) or an unknown/confederation-leading path. The movers list excludes unchanged; truncated is true when more than cap distinct movers rows existed. Errors: 400 on a missing as_id or invalid window.