{"openapi":"3.1.0","info":{"title":"BGPScout","version":"1.184.13vlbvr","description":"BGPScout internet routing intelligence - ASN/IXP/facility/CDN/traceroute search, sales prospecting, IP lookup, and account actions. This spec describes all 184 tools the server serves; if your client offers fewer, its cached copy is stale and should be regenerated from this document (#481). Tools whose description says \"prefer X\" are kept only for compatibility - call the replacement."},"servers":[{"url":"https://bgpscout.io/mcp"}],"paths":{"/call/list_tools":{"post":{"operationId":"list_tools","summary":"Discover all available BGPScout tools, grouped by category. Call this first to see the full capability set (search, lookup, prospecting, intelligence, account actions). Optional: category to filter.","description":"Discover all available BGPScout tools, grouped by category. Call this first to see the full capability set (search, lookup, prospecting, intelligence, account actions). Optional: category to filter.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"category":{"type":"string"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/lookup_asn":{"post":{"operationId":"lookup_asn","summary":"Look up detailed information about an ASN, including its PoP/facility count and IXP count. Location caveat: the record's latitude/longitude are the centroid of the registered POSTAL CODE, never the street address; the `location` field gives the resolved point with its provenance and precision, which may be a geocoded address, a data-centre facility, an exchange, or a country centroid. Do not report an approximate position as an address.","description":"Look up detailed information about an ASN, including its PoP/facility count and IXP count. Location caveat: the record's latitude/longitude are the centroid of the registered POSTAL CODE, never the street address; the `location` field gives the resolved point with its provenance and precision, which may be a geocoded address, a data-centre facility, an exchange, or a country centroid. Do not report an approximate position as an address.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"ASN number"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_facilities":{"post":{"operationId":"get_asn_facilities","summary":"List the data-center facilities/PoPs a network (ASN) is present in, with a total count. Use this to answer 'how many PoPs/facilities does ASN X have'. Sourced from PeeringDB, which is SELF-REPORTED. `pop_count` is null, never 0, when the network has no PeeringDB record or has one that lists no facilities, and `pop_count_note` says which - large carriers routinely leave the section empty (AS3356 Lumen is the reference case, #487). A null here means not disclosed; it never means the network has no PoPs. PAGING (#350/#562): `pop_count` is the FULL disclosed total and is never affected by `filter` or paging; `matched_count` is how many facilities match the current `filter` (or equals `pop_count` when no filter is given); `returned` is how many are in THIS page, and `truncated`/`next_offset` say whether more remain - a page is never passed off as a total (#501). Default limit 200 - AS6939 has 339 facilities, past the point of a useful single reply, so page through with `limit`/`offset`. FILTER: `filter` is a free-text, case- and diacritic-insensitive, multi-token AND search over name/address/city/state/country - country accepts a full name (\"Canada\") as well as the ISO code (\"CA\") - the exact matching the /asns/:asn web page's Interconnection Facilities panel runs server-side (models/panel-search.js), so this tool and that page cannot disagree about which facilities are Canadian. `filter_applied` echoes what was searched back in the response so a caller relaying a filtered count (e.g. \"16 Canadian exchanges\") cannot drop the qualifier.","description":"List the data-center facilities/PoPs a network (ASN) is present in, with a total count. Use this to answer 'how many PoPs/facilities does ASN X have'. Sourced from PeeringDB, which is SELF-REPORTED. `pop_count` is null, never 0, when the network has no PeeringDB record or has one that lists no facilities, and `pop_count_note` says which - large carriers routinely leave the section empty (AS3356 Lumen is the reference case, #487). A null here means not disclosed; it never means the network has no PoPs. PAGING (#350/#562): `pop_count` is the FULL disclosed total and is never affected by `filter` or paging; `matched_count` is how many facilities match the current `filter` (or equals `pop_count` when no filter is given); `returned` is how many are in THIS page, and `truncated`/`next_offset` say whether more remain - a page is never passed off as a total (#501). Default limit 200 - AS6939 has 339 facilities, past the point of a useful single reply, so page through with `limit`/`offset`. FILTER: `filter` is a free-text, case- and diacritic-insensitive, multi-token AND search over name/address/city/state/country - country accepts a full name (\"Canada\") as well as the ISO code (\"CA\") - the exact matching the /asns/:asn web page's Interconnection Facilities panel runs server-side (models/panel-search.js), so this tool and that page cannot disagree about which facilities are Canadian. `filter_applied` echoes what was searched back in the response so a caller relaying a filtered count (e.g. \"16 Canadian exchanges\") cannot drop the qualifier.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"ASN number"},"filter":{"type":"string","description":"Free-text filter over name/address/city/state/country, e.g. \"Canada\" or \"Equinix Chicago\". Case- and diacritic-insensitive, multi-token AND (every word must match, in any field)."},"limit":{"type":"integer","default":200,"description":"Max facilities to return in this page (post-filter). Capped at 1000."},"offset":{"type":"integer","default":0}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_ixps":{"post":{"operationId":"get_asn_ixps","summary":"List the Internet Exchange Points (IXPs) an ASN peers at, with port speeds and total interconnection capacity. One row per DISTINCT exchange, with its individual ports nested under `ports` - a network commonly has several connections to the same exchange, so `ixp_count` (exchanges) and `port_count` (connections) are different numbers and `ixp_count` is the one that matches lookup_asn (#482). `total_capacity_gbps` sums ports. `ixp_count` is null, never 0, when PeeringDB has no record or an empty one, with the reason in `ixp_count_note` (#496). Counts only ports PeeringDB flags operational=true by default; pass include_non_operational to include down ports (#502). PAGING (#350/#562): `ixp_count` is the FULL disclosed total and is never affected by `filter` or paging; `matched_count` is how many exchanges match the current `filter` (or equals `ixp_count` when no filter is given); `returned` is how many are in THIS page, and `truncated`/`next_offset` say whether more remain - a page is never passed off as a total (#501). Default limit 200 - AS6939 is on 335 exchanges, which used to come back as a single ~112KB reply with no way to narrow it. FILTER: `filter` is a free-text, case- and diacritic-insensitive, multi-token AND search over exchange name/city/country (country accepts a full name like \"Canada\" as well as the ISO code \"CA\") and each exchange's peering-LAN IPv4/IPv6 addresses - the exact matching the /asns/:asn web page's Exchange Points panel runs server-side (models/panel-search.js), so `filter: \"Canada\"` here returns the same 16-of-335 set the page does. `filter_applied` echoes what was searched back in the response so a caller relaying a filtered count cannot drop the qualifier.","description":"List the Internet Exchange Points (IXPs) an ASN peers at, with port speeds and total interconnection capacity. One row per DISTINCT exchange, with its individual ports nested under `ports` - a network commonly has several connections to the same exchange, so `ixp_count` (exchanges) and `port_count` (connections) are different numbers and `ixp_count` is the one that matches lookup_asn (#482). `total_capacity_gbps` sums ports. `ixp_count` is null, never 0, when PeeringDB has no record or an empty one, with the reason in `ixp_count_note` (#496). Counts only ports PeeringDB flags operational=true by default; pass include_non_operational to include down ports (#502). PAGING (#350/#562): `ixp_count` is the FULL disclosed total and is never affected by `filter` or paging; `matched_count` is how many exchanges match the current `filter` (or equals `ixp_count` when no filter is given); `returned` is how many are in THIS page, and `truncated`/`next_offset` say whether more remain - a page is never passed off as a total (#501). Default limit 200 - AS6939 is on 335 exchanges, which used to come back as a single ~112KB reply with no way to narrow it. FILTER: `filter` is a free-text, case- and diacritic-insensitive, multi-token AND search over exchange name/city/country (country accepts a full name like \"Canada\" as well as the ISO code \"CA\") and each exchange's peering-LAN IPv4/IPv6 addresses - the exact matching the /asns/:asn web page's Exchange Points panel runs server-side (models/panel-search.js), so `filter: \"Canada\"` here returns the same 16-of-335 set the page does. `filter_applied` echoes what was searched back in the response so a caller relaying a filtered count cannot drop the qualifier.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"include_non_operational":{"type":"boolean","default":false},"filter":{"type":"string","description":"Free-text filter over exchange name/city/country and peering IPs, e.g. \"Canada\" or \"Equinix Chicago\". Case- and diacritic-insensitive, multi-token AND."},"limit":{"type":"integer","default":200,"description":"Max exchanges to return in this page (post-filter). Capped at 1000."},"offset":{"type":"integer","default":0}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_recommended_ixps":{"post":{"operationId":"get_recommended_ixps","summary":"IXPs this ASN should consider joining: exchanges seen in its traceroute paths, filtered to ones it can actually reach - a facility this ASN already has a presence in (tier 'facility'), or a city it already has a facility in (tier 'metro'). A candidate that clears neither is DROPPED, not ranked lower, however often it appears in a traceroute; traceroute-hop frequency is only a tie-break WITHIN a tier. An empty result with a non-zero raw_candidate_count means real candidates existed but none were physically reachable - a real finding, not missing data. Same ranking function as the ASN detail page's Recommended IXPs panel.","description":"IXPs this ASN should consider joining: exchanges seen in its traceroute paths, filtered to ones it can actually reach - a facility this ASN already has a presence in (tier 'facility'), or a city it already has a facility in (tier 'metro'). A candidate that clears neither is DROPPED, not ranked lower, however often it appears in a traceroute; traceroute-hop frequency is only a tie-break WITHIN a tier. An empty result with a non-zero raw_candidate_count means real candidates existed but none were physically reachable - a real finding, not missing data. Same ranking function as the ASN detail page's Recommended IXPs panel.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_recommended_peers":{"post":{"operationId":"get_recommended_peers","summary":"Networks this ASN should consider peering with: candidates ranked by shared-infrastructure tier (shares a facility > already on a shared exchange > reachable via an IXP from get_recommended_ixps), with PeeringDB's disclosed peering policy (open/selective/restrictive/no, plus whether a balanced traffic ratio or a signed contract is required) as a secondary ranking signal and traceroute-hop frequency only as the final tie-break. A candidate clearing no tier is DROPPED, not ranked lower. peering_policy is null (not 'Restrictive') when PeeringDB has no disclosed answer - never treat null as a refusal. Same ranking function as the ASN detail page's Recommended Peers panel.","description":"Networks this ASN should consider peering with: candidates ranked by shared-infrastructure tier (shares a facility > already on a shared exchange > reachable via an IXP from get_recommended_ixps), with PeeringDB's disclosed peering policy (open/selective/restrictive/no, plus whether a balanced traffic ratio or a signed contract is required) as a secondary ranking signal and traceroute-hop frequency only as the final tie-break. A candidate clearing no tier is DROPPED, not ranked lower. peering_policy is null (not 'Restrictive') when PeeringDB has no disclosed answer - never treat null as a refusal. Same ranking function as the ASN detail page's Recommended Peers panel.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/recommend_transit":{"post":{"operationId":"recommend_transit","summary":"Rank candidate Tier-1 transit providers by MEASURED round-trip latency from a source city to named destinations (Amazon, Google, Cloudflare, a named ISP, a literal ASN, an IP address, or a hostname) - answers 'from here, what transit gets me the best path to X'. RTT comes from the last RESPONDING hop of each matching traceroute (never from geolocating the destination IP - anycast destinations like Cloudflare/Google/Amazon geolocate to a head office, not to the site that actually answered, so this tool never reads destination geography at all). A candidate only appears under `ranked` for a target once it clears an evidence floor (min_observations distinct traces from min_sources distinct source ASNs, each with a usable RTT) - below that it is listed under `insufficient_evidence` with the exact shortfall, never silently ranked on a thin sample (#336/#337/#514). `no_data` lists candidates with zero matching traces at all - absence here means 'not measured', not 'no good path' (#67). `buyer_reachable` is set from your org's declared facility footprint (get_seller_profile) when you belong to one, else null - it is a cross-connect availability signal, not part of the ranking. An IP or hostname target is resolved LIVE (DNS + currently-announced covering BGP prefix, up to 10 per call, run in parallel) rather than from a corpus, so the resolved network is current as of the call - each such target's `resolution` field says how and when. This is evidence for a buying decision, not the decision itself: the platform holds no pricing, contract terms or port availability.","description":"Rank candidate Tier-1 transit providers by MEASURED round-trip latency from a source city to named destinations (Amazon, Google, Cloudflare, a named ISP, a literal ASN, an IP address, or a hostname) - answers 'from here, what transit gets me the best path to X'. RTT comes from the last RESPONDING hop of each matching traceroute (never from geolocating the destination IP - anycast destinations like Cloudflare/Google/Amazon geolocate to a head office, not to the site that actually answered, so this tool never reads destination geography at all). A candidate only appears under `ranked` for a target once it clears an evidence floor (min_observations distinct traces from min_sources distinct source ASNs, each with a usable RTT) - below that it is listed under `insufficient_evidence` with the exact shortfall, never silently ranked on a thin sample (#336/#337/#514). `no_data` lists candidates with zero matching traces at all - absence here means 'not measured', not 'no good path' (#67). `buyer_reachable` is set from your org's declared facility footprint (get_seller_profile) when you belong to one, else null - it is a cross-connect availability signal, not part of the ranking. An IP or hostname target is resolved LIVE (DNS + currently-announced covering BGP prefix, up to 10 per call, run in parallel) rather than from a corpus, so the resolved network is current as of the call - each such target's `resolution` field says how and when. This is evidence for a buying decision, not the decision itself: the platform holds no pricing, contract terms or port availability.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"city":{"type":"string","description":"Source city, substring match"},"country":{"type":"string","description":"Source country, ISO 2-letter code"},"targets":{"type":"array","items":{"type":"string"},"description":"Destinations to rank against: a named group ('amazon', 'google', 'cloudflare', 'microsoft', 'akamai', 'fastly', 'opendns', 'quad9', 'automattic', 'bell', 'telus', 'rogers', 'shaw', 'videotron', 'comcast', 'charter', 'cox', 'att', 'verizon'), a literal ASN number as a string, an IP address, or a hostname (resolved live via DNS, up to 10 per call)."},"candidates":{"type":"array","items":{"type":"integer"},"description":"Candidate carrier ASNs to evaluate. Default: the 11 curated Tier-1s (same set as get_tier1_comparison)."},"min_observations":{"type":"integer","default":5,"description":"Evidence floor: minimum distinct traces per (candidate, target) cell."},"min_sources":{"type":"integer","default":2,"description":"Evidence floor: minimum distinct source ASNs per cell, so one probe cannot carry a ranking alone."}},"required":["city","targets"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_cdn_caches":{"post":{"operationId":"get_asn_cdn_caches","summary":"List the CDN edge caches hosted inside an ASN, grouped by provider with cache locations. Located providers: Google GGC, Netflix OCA, Meta FNA, Akamai. AWS CloudFront, Microsoft and Apple caches are reported separately in providers_present_without_locations - we know the ASN hosts them but never collected node locations (#191). total_caches counts LOCATED nodes only, so an ASN hosting only Apple caches reports 0 there and names Apple in that list; do not read total_caches: 0 as 'no caches'.","description":"List the CDN edge caches hosted inside an ASN, grouped by provider with cache locations. Located providers: Google GGC, Netflix OCA, Meta FNA, Akamai. AWS CloudFront, Microsoft and Apple caches are reported separately in providers_present_without_locations - we know the ASN hosts them but never collected node locations (#191). total_caches counts LOCATED nodes only, so an ASN hosting only Apple caches reports 0 there and names Apple in that list; do not read total_caches: 0 as 'no caches'.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_ddos_protected":{"post":{"operationId":"find_ddos_protected","summary":"Networks observed sitting behind a blanket DDoS scrubbing provider (Akamai Prolexic, Cloudflare Magic Transit, Global Secure Layer, Path, Imperva). Their routes were seen transiting a provider whose product IS the BGP announcement, so the service was live in the routing snapshot - this is evidence, not a contract, and absence does not mean unprotected. Distinct from the 'ddosm' tag, which means a network SELLS mitigation. Set exclusive=true for networks whose only upstreams are scrubbers.","description":"Networks observed sitting behind a blanket DDoS scrubbing provider (Akamai Prolexic, Cloudflare Magic Transit, Global Secure Layer, Path, Imperva). Their routes were seen transiting a provider whose product IS the BGP announcement, so the service was live in the routing snapshot - this is evidence, not a contract, and absence does not mean unprotected. Distinct from the 'ddosm' tag, which means a network SELLS mitigation. Set exclusive=true for networks whose only upstreams are scrubbers.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"provider_asn":{"type":"integer","description":"Only networks behind this provider"},"country":{"type":"string","description":"ISO 2-letter code"},"exclusive":{"type":"boolean","description":"Only networks with no non-scrubbing upstream"},"limit":{"type":"integer","default":100}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_transit_providers":{"post":{"operationId":"get_transit_providers","summary":"An ASN's upstreams from two sources: traceroute-observed Tier-1 handoffs (narrow, ~490 ASNs, but carries the handoff city) and BGP AS_PATH adjacency (far wider coverage, no location - the response's bgp_upstreams note gives the real shape rather than a number that would go stale). AS_PATH does not label relationships, so a BGP adjacency may be transit or peering.","description":"An ASN's upstreams from two sources: traceroute-observed Tier-1 handoffs (narrow, ~490 ASNs, but carries the handoff city) and BGP AS_PATH adjacency (far wider coverage, no location - the response's bgp_upstreams note gives the real shape rather than a number that would go stale). AS_PATH does not label relationships, so a BGP adjacency may be transit or peering.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_asns":{"post":{"operationId":"search_asns","summary":"Search ASNs with rich filters (name/asn, country, state, city, rir, traffic level, IPv6, on-IX, MANRS, RPKI, network type, asn_type flag, CDN provider, min/max PoPs/IXPs/peers/downstreams). PEERS CAVEAT (#264/#422): peer/downstream bounds and their sorts read counts of observed BGP AS_PATH adjacencies, which is NOT a peer list - it conflates transit, settlement-free peering and sibling relationships. Counts come from a curated multi-collector RouteViews + RIPE RIS build (#208); get_asn_neighbours returns the exact collector set and build date in observed_from. They are FLOORS, not censuses, and more collectors does not lift that ceiling much: a RIB holds the best path per prefix per collector peer, so peering that is never the best path anywhere (the normal case for IXP route-server peering) is invisible to every collector. Measured 2026-08-24: ASNs that are route-server peers at a single exchange with 200+ other route-server peers on it show a mean of 6.69 observed adjacencies here. Do not state a peer count as a fact without that caveat. An ASN with no observed adjacency at all is UNOBSERVED, not zero - it is silently excluded from min/max peer and downstream filters rather than being treated as zero, and both sorts place unobserved ASNs last regardless of direction (#67). Downstream direction means observed nearer-to-origin AS_PATH adjacency; it is not proof of a commercial customer relationship.","description":"Search ASNs with rich filters (name/asn, country, state, city, rir, traffic level, IPv6, on-IX, MANRS, RPKI, network type, asn_type flag, CDN provider, min/max PoPs/IXPs/peers/downstreams). PEERS CAVEAT (#264/#422): peer/downstream bounds and their sorts read counts of observed BGP AS_PATH adjacencies, which is NOT a peer list - it conflates transit, settlement-free peering and sibling relationships. Counts come from a curated multi-collector RouteViews + RIPE RIS build (#208); get_asn_neighbours returns the exact collector set and build date in observed_from. They are FLOORS, not censuses, and more collectors does not lift that ceiling much: a RIB holds the best path per prefix per collector peer, so peering that is never the best path anywhere (the normal case for IXP route-server peering) is invisible to every collector. Measured 2026-08-24: ASNs that are route-server peers at a single exchange with 200+ other route-server peers on it show a mean of 6.69 observed adjacencies here. Do not state a peer count as a fact without that caveat. An ASN with no observed adjacency at all is UNOBSERVED, not zero - it is silently excluded from min/max peer and downstream filters rather than being treated as zero, and both sorts place unobserved ASNs last regardless of direction (#67). Downstream direction means observed nearer-to-origin AS_PATH adjacency; it is not proof of a commercial customer relationship.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"country":{"type":"string","description":"2-letter country code"},"state":{"type":"string"},"city":{"type":"string"},"rir":{"type":"string"},"traffic_level":{"type":"string"},"has_ipv6":{"type":"boolean","description":"ANNOUNCES IPv6 prefixes. Not evidence that IPv6 works - use ipv6_reachability for that."},"ipv6_reachability":{"type":"string","description":"Evidence that IPv6 works (#54): confirmed | tcp6 | icmp6 | path | aaaa | announced_only | unconfirmed. The unconfirmed values select networks we have NOT observed working - mostly never probed - never networks whose IPv6 is broken."},"on_ix":{"type":"boolean"},"manrs":{"type":"boolean"},"rpki":{"type":"boolean","description":"ROV OBSERVED, not \"has RPKI\". true selects networks bgp.tools has seen dropping ROA-invalid routes; false selects networks it has NOT, which is mostly networks with no RIPE Atlas probe behind them rather than networks that do not validate. Says nothing about whether the network publishes ROAs for its own prefixes (#490)."},"network_type":{"type":"string"},"asn_type":{"type":"string","description":"mobile|dsl|cdn|gov|corp|uni|biznet|satnet|anycast|vpn|tor"},"cdn_provider":{"type":"string","description":"ggc|oca|fna|akamai|aws_cloudfront|microsoft|apple"},"min_pops":{"type":"integer"},"max_pops":{"type":"integer"},"min_ixps":{"type":"integer"},"max_ixps":{"type":"integer"},"min_peers":{"type":"integer","description":"Observed BGP adjacencies, both directions summed. See the tool-level PEERS CAVEAT."},"max_peers":{"type":"integer","description":"Same caveat as min_peers - also silently excludes never-observed ASNs, which may in reality have few or no peers (#208)."},"min_downstream":{"type":"integer","description":"Observed adjacencies where this ASN appeared nearer the collector; not proven customers."},"max_downstream":{"type":"integer","description":"Same collector/relationship caveat as min_downstream."},"sort":{"type":"string","description":"asn|name|country|traffic|peers|downstream"},"limit":{"type":"integer","default":20}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_traces":{"post":{"operationId":"get_asn_traces","summary":"Get traceroute data for an ASN","description":"Get traceroute data for an ASN","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"limit":{"type":"integer","default":10}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_ixp_info":{"post":{"operationId":"get_ixp_info","summary":"Search Internet Exchange Points by name/city/country/operator, with min_peers/max_peers/min_facilities filters - this IS the /ixps browse listing (#350), not a lookup that requires already knowing the PeeringDB id. Each result carries its operator (the PeeringDB organisation that runs it - always present), where we could establish one the registry organisation behind it with the method and confidence that link rests on, and a `virtual` classification (score/tier/reasons) saying how likely the exchange is a virtual fabric rather than a physical one. DEFAULTS: min_peers = 1, so exchanges with no membership recorded in PeeringDB are excluded - pass min_peers=0 to include them (they are often defunct rather than never-real, which is itself informative). exclude_virtual defaults to FALSE: virtual exchanges are returned, labelled, unless you ask for them to be dropped. The website's /ixps page applies the same min_peers default. operator_org.org_url is the canonical browser link for that registry organisation (models/org-slug.js) - the same page a person clicking it on the site lands on; it is null when the org's registry identity could not be resolved, and operator_org itself is null when no registry link was established at all. It replaces the old `url` field, which was hand-built as /orgs/{registry}/{org_key} - the documented fallback form, which resolves but is not the canonical link.","description":"Search Internet Exchange Points by name/city/country/operator, with min_peers/max_peers/min_facilities filters - this IS the /ixps browse listing (#350), not a lookup that requires already knowing the PeeringDB id. Each result carries its operator (the PeeringDB organisation that runs it - always present), where we could establish one the registry organisation behind it with the method and confidence that link rests on, and a `virtual` classification (score/tier/reasons) saying how likely the exchange is a virtual fabric rather than a physical one. DEFAULTS: min_peers = 1, so exchanges with no membership recorded in PeeringDB are excluded - pass min_peers=0 to include them (they are often defunct rather than never-real, which is itself informative). exclude_virtual defaults to FALSE: virtual exchanges are returned, labelled, unless you ask for them to be dropped. The website's /ixps page applies the same min_peers default. operator_org.org_url is the canonical browser link for that registry organisation (models/org-slug.js) - the same page a person clicking it on the site lands on; it is null when the org's registry identity could not be resolved, and operator_org itself is null when no registry link was established at all. It replaces the old `url` field, which was hand-built as /orgs/{registry}/{org_key} - the documented fallback form, which resolves but is not the canonical link.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"city":{"type":"string","description":"Substring match against the exchange city, separate from name so a caller can filter by one dimension without OR-ing them (#350)."},"country":{"type":"string"},"operator":{"type":"string","description":"Operator organisation name, substring match - e.g. \"Equinix\", \"DE-CIX\""},"min_peers":{"type":"integer","description":"Defaults to 1. Pass 0 to include exchanges with no members listed in PeeringDB."},"max_peers":{"type":"integer"},"min_facilities":{"type":"integer"},"exclude_virtual":{"type":"boolean","description":"Drop exchanges classified as likely virtual (see the `virtual` field on each result). Default false."},"sort":{"type":"string","description":"peers|facilities|name"},"limit":{"type":"integer","default":30},"offset":{"type":"integer","default":0,"description":"Paginate past `limit` for a full browse of the listing (#350)."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_ixp_members":{"post":{"operationId":"get_ixp_members","summary":"List the ASNs that peer at a given IXP (by its PeeringDB id from get_ixp_info). ONE ROW PER MEMBER, with that member's ports nested under it - a member with two ports is one row, not two. Each port carries its IPv4/IPv6 peering-LAN address and, where the operator declared one in PeeringDB, the facility (building) that port terminates in. A port with no facility is UNDECLARED, not \"in no facility\". PAGING: `member_count` is the FULL membership, `returned` is how many are in this page, and `truncated`/`next_offset` say whether there are more - a page is never passed off as the total (#501). OPERATIONAL: members and capacity count only ports PeeringDB flags operational=true, so a member whose every port is down is absent; pass include_non_operational for the historical shape (#502).","description":"List the ASNs that peer at a given IXP (by its PeeringDB id from get_ixp_info). ONE ROW PER MEMBER, with that member's ports nested under it - a member with two ports is one row, not two. Each port carries its IPv4/IPv6 peering-LAN address and, where the operator declared one in PeeringDB, the facility (building) that port terminates in. A port with no facility is UNDECLARED, not \"in no facility\". PAGING: `member_count` is the FULL membership, `returned` is how many are in this page, and `truncated`/`next_offset` say whether there are more - a page is never passed off as the total (#501). OPERATIONAL: members and capacity count only ports PeeringDB flags operational=true, so a member whose every port is down is absent; pass include_non_operational for the historical shape (#502).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ix_id":{"type":"integer"},"limit":{"type":"integer","default":200},"offset":{"type":"integer","default":0},"include_non_operational":{"type":"boolean","default":false}},"required":["ix_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_facility_info":{"post":{"operationId":"get_facility_info","summary":"Search data centers/facilities by name/country/city, with a count of how many ASNs are present - this IS the /facilities browse listing (#350), not a lookup that requires already knowing the PeeringDB id.","description":"Search data centers/facilities by name/country/city, with a count of how many ASNs are present - this IS the /facilities browse listing (#350), not a lookup that requires already knowing the PeeringDB id.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"country":{"type":"string"},"city":{"type":"string"},"min_asns":{"type":"integer"},"sort":{"type":"string","description":"asns|name"},"limit":{"type":"integer","default":20},"offset":{"type":"integer","default":0,"description":"Paginate past `limit` for a full browse of the listing (#350)."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/lookup_ip":{"post":{"operationId":"lookup_ip","summary":"Geolocate an IP address (city/country/coordinates) and, when available, the ASN that announces it. `bogon` is set (and `geo`/`asn` are null) for IANA special-use/reserved space such as documentation ranges or RFC 1918 - these resolve immediately rather than paying for a routing-table scan guaranteed to miss (#480).","description":"Geolocate an IP address (city/country/coordinates) and, when available, the ASN that announces it. `bogon` is set (and `geo`/`asn` are null) for IANA special-use/reserved space such as documentation ranges or RFC 1918 - these resolve immediately rather than paying for a routing-table scan guaranteed to miss (#480).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ip":{"type":"string"}},"required":["ip"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_facility_members":{"post":{"operationId":"get_facility_members","summary":"List the ASNs present in a data-center facility (by its PeeringDB id from get_facility_info). Returns an OBJECT, not a bare array: `members` plus `member_count` (the full total), `returned`, `limit`, `offset`, `truncated` and `next_offset`. It used to return a bare array that silently stopped at the limit, so a 721-ASN facility came back as 200 rows with nothing saying so (#501). `member_count` now matches get_facility_info's asn_count exactly; a tenant we hold no ASN record for is returned with `asn: null` and its PeeringDB net id rather than being dropped.","description":"List the ASNs present in a data-center facility (by its PeeringDB id from get_facility_info). Returns an OBJECT, not a bare array: `members` plus `member_count` (the full total), `returned`, `limit`, `offset`, `truncated` and `next_offset`. It used to return a bare array that silently stopped at the limit, so a 721-ASN facility came back as 200 rows with nothing saying so (#501). `member_count` now matches get_facility_info's asn_count exactly; a tenant we hold no ASN record for is returned with `asn: null` and its PeeringDB net id rather than being dropped.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"facility_id":{"type":"integer"},"limit":{"type":"integer","default":200},"offset":{"type":"integer","default":0}},"required":["facility_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_facility_events":{"post":{"operationId":"get_facility_events","summary":"One building's change feed and the exchanges inside it - the same data as the /facilities/<id> page (#27). Returns the facility record, the IXPs physically in it, and the merged event log for both, with counts so a capped feed is never mistaken for the total. Events reached via an exchange are labelled: an IXP join at a multi-site exchange is not a statement about this one building.","description":"One building's change feed and the exchanges inside it - the same data as the /facilities/<id> page (#27). Returns the facility record, the IXPs physically in it, and the merged event log for both, with counts so a capped feed is never mistaken for the total. Events reached via an exchange are labelled: an IXP join at a multi-site exchange is not a statement about this one building.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"facility_id":{"type":"integer","description":"PeeringDB facility id, from get_facility_info"},"limit":{"type":"integer","default":100}},"required":["facility_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_org_asns":{"post":{"operationId":"get_org_asns","summary":"List all ASNs under the same organization. Pass an asn (finds its siblings via the registry allocation record, same identity the /orgs pages use) or an org_id (asn.org_id - a shorter, registry-specific handle like ARIN's 'MCICS', NOT the same identifier as the /orgs URL's opaque_id; used only as a fallback when no asn is given). Same sort/filter/pagination #288 added to the org detail page: sort (advertised|prefixes|peers|asn|name, default advertised), dir (asc|desc), filter (all|announcing|dormant - 'announcing' means observed announcing IPv4 in the DFZ, not merely delegated), page. NOTE (#564): filter=dormant here means an ASN NEVER OBSERVED ANNOUNCING IPv4 - a fact about the ASN's activity, not about address space. It is unrelated to search_dormant_ipv4's dormant_addresses, which measures unadvertised IPv4 address space held by the org regardless of which (if any) ASN announces the rest. An org can be 100% address-utilized and still have a dormant ASN here, or have every ASN announcing and still hold a dormant /12 there. advertised_ipv4/prefix_count/peer_count are null (not zero) for an ASN never observed announcing or with no peer-count row - a registry delegation is not evidence of activity. org_url is the canonical browser link (models/org-slug.js) when the org's registry identity could be resolved, else null.","description":"List all ASNs under the same organization. Pass an asn (finds its siblings via the registry allocation record, same identity the /orgs pages use) or an org_id (asn.org_id - a shorter, registry-specific handle like ARIN's 'MCICS', NOT the same identifier as the /orgs URL's opaque_id; used only as a fallback when no asn is given). Same sort/filter/pagination #288 added to the org detail page: sort (advertised|prefixes|peers|asn|name, default advertised), dir (asc|desc), filter (all|announcing|dormant - 'announcing' means observed announcing IPv4 in the DFZ, not merely delegated), page. NOTE (#564): filter=dormant here means an ASN NEVER OBSERVED ANNOUNCING IPv4 - a fact about the ASN's activity, not about address space. It is unrelated to search_dormant_ipv4's dormant_addresses, which measures unadvertised IPv4 address space held by the org regardless of which (if any) ASN announces the rest. An org can be 100% address-utilized and still have a dormant ASN here, or have every ASN announcing and still hold a dormant /12 there. advertised_ipv4/prefix_count/peer_count are null (not zero) for an ASN never observed announcing or with no peer-count row - a registry delegation is not evidence of activity. org_url is the canonical browser link (models/org-slug.js) when the org's registry identity could be resolved, else null.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"org_id":{"type":"string"},"sort":{"type":"string","description":"advertised|prefixes|peers|asn|name"},"dir":{"type":"string","description":"asc|desc"},"filter":{"type":"string","description":"all|announcing|dormant"},"page":{"type":"integer","default":1},"limit":{"type":"integer","default":100}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_transit_customers":{"post":{"operationId":"find_transit_customers","summary":"Networks observed DOWNSTREAM of a given transit ASN (reverse of get_transit_providers). Returns traceroute-observed handoffs (with cities) and BGP AS_PATH downstreams (far wider coverage, no location). NEITHER list proves a commercial relationship: both are routing observations, and a downstream position is equally consistent with paid transit, settlement-free peering, a sibling network, or transit supplied by an upstream on that network's behalf. Do not report these networks as customers of, or as buying transit from, the queried ASN - only as observed downstream of it (#499). The tool name is retained for compatibility and overstates what the data shows.","description":"Networks observed DOWNSTREAM of a given transit ASN (reverse of get_transit_providers). Returns traceroute-observed handoffs (with cities) and BGP AS_PATH downstreams (far wider coverage, no location). NEITHER list proves a commercial relationship: both are routing observations, and a downstream position is equally consistent with paid transit, settlement-free peering, a sibling network, or transit supplied by an upstream on that network's behalf. Do not report these networks as customers of, or as buying transit from, the queried ASN - only as observed downstream of it (#499). The tool name is retained for compatibility and overstates what the data shows.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"transit_asn":{"type":"integer"},"limit":{"type":"integer","default":100}},"required":["transit_asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_co_located":{"post":{"operationId":"find_co_located","summary":"Networks that share a data-center facility with a given ASN (cross-connect / peering candidates)","description":"Networks that share a data-center facility with a given ASN (cross-connect / peering candidates)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"limit":{"type":"integer","default":100}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_near":{"post":{"operationId":"find_near","summary":"Find facilities, IXPs and CDN caches within a radius of a location (latitude/longitude in degrees, radius in km)","description":"Find facilities, IXPs and CDN caches within a radius of a location (latitude/longitude in degrees, radius in km)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":50},"limit":{"type":"integer","default":30}},"required":["latitude","longitude"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_networks_near":{"post":{"operationId":"find_networks_near","summary":"Networks (ASNs) with IP address presence in an area - the geolocated IP footprint behind the IP-density heatmap. Use for 'which networks have IPs in/around <place>'. Ranked by number of geolocated IPs. Give either latitude+longitude (+radius_km) or a city and/or country. Coverage: ~6.5k ASNs with enough geolocated space to map; a network absent here may still operate there.","description":"Networks (ASNs) with IP address presence in an area - the geolocated IP footprint behind the IP-density heatmap. Use for 'which networks have IPs in/around <place>'. Ranked by number of geolocated IPs. Give either latitude+longitude (+radius_km) or a city and/or country. Coverage: ~6.5k ASNs with enough geolocated space to map; a network absent here may still operate there.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":50},"city":{"type":"string"},"country":{"type":"string","description":"ISO 2-letter code"},"min_ips":{"type":"integer","description":"Only networks with at least this many geolocated IPs in the area"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_transit_near":{"post":{"operationId":"find_transit_near","summary":"Networks (ASNs) observed taking transit handoffs in an area, and from which Tier-1, derived from traceroute data. Use for 'which ASNs have transit locations in/around <place>'. Give latitude+longitude (+radius_km) or a city/country. Default grain is one row per asn/transit_asn/city - an asn/transit pair seen handing off in five cities returns five rows, not one row summed across all of them. Pass aggregate_by: 'provider_pair' for a national rollup instead: one row per asn/transit_asn with `observations` summed, `city_count`, and a `cities` breakdown; `city`/`country` are only present on that row when city_count is 1. Coverage is narrow - only ~490 ASNs across 11 Tier-1 providers currently have observed handoffs, so absence is not evidence of absence.","description":"Networks (ASNs) observed taking transit handoffs in an area, and from which Tier-1, derived from traceroute data. Use for 'which ASNs have transit locations in/around <place>'. Give latitude+longitude (+radius_km) or a city/country. Default grain is one row per asn/transit_asn/city - an asn/transit pair seen handing off in five cities returns five rows, not one row summed across all of them. Pass aggregate_by: 'provider_pair' for a national rollup instead: one row per asn/transit_asn with `observations` summed, `city_count`, and a `cities` breakdown; `city`/`country` are only present on that row when city_count is 1. Coverage is narrow - only ~490 ASNs across 11 Tier-1 providers currently have observed handoffs, so absence is not evidence of absence.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":100},"city":{"type":"string"},"country":{"type":"string"},"transit_asn":{"type":"integer","description":"Only handoffs from this upstream"},"aggregate_by":{"type":"string","description":"'provider_pair' to roll every city up into one national row per asn/transit_asn, with a cities[] breakdown. Omit for one row per city (default)."},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_upstreams_near":{"post":{"operationId":"find_upstreams_near","summary":"Networks (ASNs) present in an area together with their BGP upstreams. Use for 'which networks around <place> buy transit, and from whom', or with upstream_asn for 'who near <place> is behind <provider>'. Far wider coverage than find_transit_near, but the two halves come from different sources: location is PeeringDB facility presence, upstreams are BGP AS_PATH adjacency - an upstream listed here was NOT observed handing off in that city, and AS_PATH cannot distinguish transit from peering, and the coverage is a floor that does not grow much with more collectors (#208). Use find_transit_near instead when the handoff location itself must be real.","description":"Networks (ASNs) present in an area together with their BGP upstreams. Use for 'which networks around <place> buy transit, and from whom', or with upstream_asn for 'who near <place> is behind <provider>'. Far wider coverage than find_transit_near, but the two halves come from different sources: location is PeeringDB facility presence, upstreams are BGP AS_PATH adjacency - an upstream listed here was NOT observed handing off in that city, and AS_PATH cannot distinguish transit from peering, and the coverage is a floor that does not grow much with more collectors (#208). Use find_transit_near instead when the handoff location itself must be real.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":50},"city":{"type":"string"},"country":{"type":"string"},"upstream_asn":{"type":"integer","description":"Only networks adjacent to this upstream"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_networks_at_facilities_near":{"post":{"operationId":"find_networks_at_facilities_near","summary":"Networks (ASNs) with a PeeringDB data-centre/PoP presence in an area, and which facilities. Use for 'who is in a data centre in/around <place>' or to find cross-connect candidates in a metro. Give latitude+longitude (+radius_km) or a city/country. COVERAGE (#515): the radius can only be tested against facilities that have stored coordinates - a facility with memberships but no lat/long is left out of `networks`, not because it is outside the radius but because it cannot be evaluated. The response's `coverage` field reports how many candidate facilities exist, how many were geocoded, and (when you passed city/country) the specific `unlocated_matches` omitted - treat those as unresolved, not absent.","description":"Networks (ASNs) with a PeeringDB data-centre/PoP presence in an area, and which facilities. Use for 'who is in a data centre in/around <place>' or to find cross-connect candidates in a metro. Give latitude+longitude (+radius_km) or a city/country. COVERAGE (#515): the radius can only be tested against facilities that have stored coordinates - a facility with memberships but no lat/long is left out of `networks`, not because it is outside the radius but because it cannot be evaluated. The response's `coverage` field reports how many candidate facilities exist, how many were geocoded, and (when you passed city/country) the specific `unlocated_matches` omitted - treat those as unresolved, not absent.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":50},"city":{"type":"string"},"country":{"type":"string"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_networks_on_ixps_near":{"post":{"operationId":"find_networks_on_ixps_near","summary":"Networks (ASNs) peering at IXPs in an area, with port capacity. Use for 'who is reachable on an exchange in/around <place>' or to find peering candidates in a metro. IXPs are located via their facilities. Give latitude+longitude (+radius_km) or a city/country. COVERAGE (#515): an IXP present only in un-geocoded facilities cannot be placed in the radius and is left out of `networks`, not ruled outside it. The response's `coverage` field reports the same facility/membership omission accounting as find_networks_at_facilities_near, and `unlocated_matches` when you passed city/country.","description":"Networks (ASNs) peering at IXPs in an area, with port capacity. Use for 'who is reachable on an exchange in/around <place>' or to find peering candidates in a metro. IXPs are located via their facilities. Give latitude+longitude (+radius_km) or a city/country. COVERAGE (#515): an IXP present only in un-geocoded facilities cannot be placed in the radius and is left out of `networks`, not ruled outside it. The response's `coverage` field reports the same facility/membership omission accounting as find_networks_at_facilities_near, and `unlocated_matches` when you passed city/country.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":50},"city":{"type":"string"},"country":{"type":"string"},"ix_id":{"type":"integer","description":"Restrict to one IXP (PeeringDB id)"},"limit":{"type":"integer","default":100}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_allocations":{"post":{"operationId":"search_allocations","summary":"Search RIR address allocations - every IPv4/IPv6/ASN delegation the five registries published, with the date each block was handed to an organisation. Use for 'what has ARIN allocated recently', 'which blocks were assigned in <country> since <date>', or to date a prefix. Newest first by default. Each row carries org_key (the registry's opaque organisation id) and org_url, the canonical browser link for that organisation (models/org-slug.js). org_url is null where the registry published no organisation id for the block, which means the holder is unknown to us and not that the org has no page; measured 2026-08-22 that is 0 of the 434,076 allocated/assigned rows this tool can return.","description":"Search RIR address allocations - every IPv4/IPv6/ASN delegation the five registries published, with the date each block was handed to an organisation. Use for 'what has ARIN allocated recently', 'which blocks were assigned in <country> since <date>', or to date a prefix. Newest first by default. Each row carries org_key (the registry's opaque organisation id) and org_url, the canonical browser link for that organisation (models/org-slug.js). org_url is null where the registry published no organisation id for the block, which means the holder is unknown to us and not that the org has no page; measured 2026-08-22 that is 0 of the 434,076 allocated/assigned rows this tool can return.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"ipv4 | ipv6 | asn (default ipv4)"},"registry":{"type":"string","description":"arin | ripencc | apnic | lacnic | afrinic"},"country":{"type":"string","description":"ISO 2-letter code"},"allocated_from":{"type":"string","description":"YYYY-MM-DD"},"allocated_to":{"type":"string","description":"YYYY-MM-DD"},"prefix":{"type":"string","description":"Prefix or address to match/cover"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_allocations":{"post":{"operationId":"get_asn_allocations","summary":"Paged IPv4/IPv6 blocks delegated to an ASN's registry organisation. Totals and transfer/direct summaries cover the complete inventory, not only the returned page. IPv4 and IPv6 have independent cursors so one family cannot crowd out the other. SCOPE: every total is for ONE registry org handle, and a company routinely holds several - AS16509 resolves to an ARIN handle with 5 IPv4 blocks while an identically-named sibling handle holds 1,807. When siblings exist, `related_orgs` names them with the extra block counts; walk them with lookup_rir_org's org_key. `org_resolution` is 'resolved', 'unresolved' (the ASN sits in a delegation range carrying no organisation id, so the holder is unknown - counts come back null, NOT zero) or 'absent' (no delegation record at all).","description":"Paged IPv4/IPv6 blocks delegated to an ASN's registry organisation. Totals and transfer/direct summaries cover the complete inventory, not only the returned page. IPv4 and IPv6 have independent cursors so one family cannot crowd out the other. SCOPE: every total is for ONE registry org handle, and a company routinely holds several - AS16509 resolves to an ARIN handle with 5 IPv4 blocks while an identically-named sibling handle holds 1,807. When siblings exist, `related_orgs` names them with the extra block counts; walk them with lookup_rir_org's org_key. `org_resolution` is 'resolved', 'unresolved' (the ASN sits in a delegation range carrying no organisation id, so the holder is unknown - counts come back null, NOT zero) or 'absent' (no delegation record at all).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"resource_type":{"type":"string","enum":["all","ipv4","ipv6"],"default":"all"},"limit":{"type":"integer","default":100,"maximum":500},"cursor":{"type":"string","description":"Cursor when resource_type is ipv4 or ipv6"},"ipv4_cursor":{"type":"string"},"ipv6_cursor":{"type":"string"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_transfers":{"post":{"operationId":"search_transfers","summary":"The RIR transfer logs: IPv4/IPv6 blocks and ASNs that changed hands between organisations, naming both parties. transfer_type distinguishes a market transfer (the block was bought or leased) from a merger/acquisition (the holding company itself was acquired). Use for 'who has been buying address space', 'where did this prefix come from', 'what has <company> sold', or 'which ASNs were acquired in <year>'. Covers ARIN, RIPE NCC, APNIC, LACNIC and AFRINIC.","description":"The RIR transfer logs: IPv4/IPv6 blocks and ASNs that changed hands between organisations, naming both parties. transfer_type distinguishes a market transfer (the block was bought or leased) from a merger/acquisition (the holding company itself was acquired). Use for 'who has been buying address space', 'where did this prefix come from', 'what has <company> sold', or 'which ASNs were acquired in <year>'. Covers ARIN, RIPE NCC, APNIC, LACNIC and AFRINIC.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"Find transfers of this AS number"},"prefix":{"type":"string","description":"Prefix or IP; matches any transfer whose range overlaps it"},"organization":{"type":"string","description":"Substring of either the selling or the buying org name"},"transfer_type":{"type":"string","description":"market | merger"},"registry":{"type":"string","description":"arin | ripencc | apnic | lacnic | afrinic"},"since":{"type":"string","description":"YYYY-MM-DD"},"until":{"type":"string","description":"YYYY-MM-DD"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/lookup_rir_org":{"post":{"operationId":"lookup_rir_org","summary":"Registry organisations and their independently paged ASN, IPv4 and IPv6 inventories. Every family reports total, returned, truncated and next_cursor so large holders are never silently incomplete. SCOPE: a registry org handle is not a company. Totals are `scope: 'this registry org handle only'`; RIRs issue a company several handles over mergers and transfers, and 643 registered names in the corpus are carried by more than one org. `related_orgs` discloses the handles sharing this one's exact registered name plus the extra blocks they hold - disclosed, deliberately not merged, because a registry name string is not proof of common ownership. When an ASN matches no organisation, `org_resolution` separates 'unresolved' (a delegation range exists but carries no organisation id) from 'absent' (no delegation record at all). Also returns `ipv4_utilization`, the org's allocated/advertised/DORMANT IPv4 address counts (#564) - see search_dormant_ipv4's description for what 'dormant' measures and its caveats, which apply here too.","description":"Registry organisations and their independently paged ASN, IPv4 and IPv6 inventories. Every family reports total, returned, truncated and next_cursor so large holders are never silently incomplete. SCOPE: a registry org handle is not a company. Totals are `scope: 'this registry org handle only'`; RIRs issue a company several handles over mergers and transfers, and 643 registered names in the corpus are carried by more than one org. `related_orgs` discloses the handles sharing this one's exact registered name plus the extra blocks they hold - disclosed, deliberately not merged, because a registry name string is not proof of common ownership. When an ASN matches no organisation, `org_resolution` separates 'unresolved' (a delegation range exists but carries no organisation id) from 'absent' (no delegation record at all). Also returns `ipv4_utilization`, the org's allocated/advertised/DORMANT IPv4 address counts (#564) - see search_dormant_ipv4's description for what 'dormant' measures and its caveats, which apply here too.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"prefix":{"type":"string","description":"Prefix or IP the org holds"},"name":{"type":"string","description":"Organisation / network name"},"registry":{"type":"string"},"org_key":{"type":"string","description":"The registry's opaque org id, as returned by this tool"},"limit":{"type":"integer","default":100,"maximum":500},"ipv4_cursor":{"type":"string"},"ipv6_cursor":{"type":"string"},"asn_cursor":{"type":"string"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_dormant_ipv4":{"post":{"operationId":"search_dormant_ipv4","summary":"Find registry organisations holding significant unadvertised (\"dormant\") IPv4 space - the lease/transfer/sales prospecting signal #175 and #302 built for the /orgs page, now reachable from MCP (#564). WHAT 'DORMANT' MEASURES, EXACTLY: an address is dormant if it was NOT covered by any BGP route we observed in ANY of the last `window_days` nightly snapshots (7 by default). It is a MEASUREMENT, not a verdict - dormant is not the same as unused or available: the space can be internal-only, reserved, mid-migration, or announced solely to peers outside our DFZ view. Every result carries `nights_observed` (how many of those nights actually contributed - render this, not just window_days, #67) and `measured_at`. ORG-LEVEL BY DESIGN, NOT ASN-LEVEL (#175): the announcing ASN is irrelevant to whether an org's address is routed at all - summing per-ASN figures undercounts an org whenever its org-to-ASN linkage is incomplete, which #175 measured making Amazon read as 20.8% dormant (an artifact of only 4 of its ASNs being linked) versus the correct org-level 84.8%. So this tool returns organisations, never ASNs; to ask 'how much dormant space is behind ASN X', call get_asn_allocations or lookup_rir_org for that ASN's org_key, which each carry the same ipv4_utilization figures attributed to the ORG. DISTINCT FROM get_org_asns's filter=dormant: that means an ASN never observed announcing IPv4 at all (a fact about the ASN), completely different from address-space dormancy (a fact about the org's allocated blocks) - an org can be 100% utilized and still hold a dormant ASN, or fully-announcing ASNs and still sit on a dormant /12. COVERAGE (#67): every response carries `coverage.orgs_computed` vs `coverage.orgs_holding_ipv4` - an org the nightly job has not reached yet is NOT a 0%-dormant org, it is unmeasured, and cannot appear in these results at all until it is computed. `country` filters/returns rir_org.country, which is DERIVED FROM THE ORG'S OWN ASNs (#399) - it is not the registered country of any one block, and only resolves for orgs holding at least one named ASN.","description":"Find registry organisations holding significant unadvertised (\"dormant\") IPv4 space - the lease/transfer/sales prospecting signal #175 and #302 built for the /orgs page, now reachable from MCP (#564). WHAT 'DORMANT' MEASURES, EXACTLY: an address is dormant if it was NOT covered by any BGP route we observed in ANY of the last `window_days` nightly snapshots (7 by default). It is a MEASUREMENT, not a verdict - dormant is not the same as unused or available: the space can be internal-only, reserved, mid-migration, or announced solely to peers outside our DFZ view. Every result carries `nights_observed` (how many of those nights actually contributed - render this, not just window_days, #67) and `measured_at`. ORG-LEVEL BY DESIGN, NOT ASN-LEVEL (#175): the announcing ASN is irrelevant to whether an org's address is routed at all - summing per-ASN figures undercounts an org whenever its org-to-ASN linkage is incomplete, which #175 measured making Amazon read as 20.8% dormant (an artifact of only 4 of its ASNs being linked) versus the correct org-level 84.8%. So this tool returns organisations, never ASNs; to ask 'how much dormant space is behind ASN X', call get_asn_allocations or lookup_rir_org for that ASN's org_key, which each carry the same ipv4_utilization figures attributed to the ORG. DISTINCT FROM get_org_asns's filter=dormant: that means an ASN never observed announcing IPv4 at all (a fact about the ASN), completely different from address-space dormancy (a fact about the org's allocated blocks) - an org can be 100% utilized and still hold a dormant ASN, or fully-announcing ASNs and still sit on a dormant /12. COVERAGE (#67): every response carries `coverage.orgs_computed` vs `coverage.orgs_holding_ipv4` - an org the nightly job has not reached yet is NOT a 0%-dormant org, it is unmeasured, and cannot appear in these results at all until it is computed. `country` filters/returns rir_org.country, which is DERIVED FROM THE ORG'S OWN ASNs (#399) - it is not the registered country of any one block, and only resolves for orgs holding at least one named ASN.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"min_dormant_addresses":{"type":"integer","description":"Minimum total dormant IPv4 addresses held by the org"},"min_largest_block":{"type":"integer","description":"Minimum size of the single largest dormant CIDR - the leasable-unit filter"},"max_utilization_pct":{"type":"number","description":"Only orgs at or below this advertised/allocated percentage (0-100)"},"min_utilization_pct":{"type":"number","description":"0-100"},"registry":{"type":"string","description":"arin | ripencc | apnic | lacnic | afrinic"},"country":{"type":"string","description":"ISO 2-letter code. ASN-derived (#399), not the block's registered country - see description."},"sort":{"type":"string","enum":["dormant","largest_block","utilization"],"default":"dormant"},"dir":{"type":"string","enum":["asc","desc"],"default":"desc"},"page":{"type":"integer","default":1},"limit":{"type":"integer","default":50,"maximum":500}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_dormancy_by_registry":{"post":{"operationId":"get_dormancy_by_registry","summary":"Global IPv4 dormancy totals grouped by registry (ARIN/RIPE NCC/APNIC/LACNIC/AFRINIC) - the 'which RIR has the most dormant space' rollup (#564), for a headline or a per-registry comparison rather than a per-org search (see search_dormant_ipv4 for that, and for what 'dormant' measures - the same 7-night-window definition and caveats apply here). Five rows, computed at request time from org_ipv4_utilization (already indexed on registry, no new collection). Each row reports orgs measured, allocated/advertised/dormant address totals and dormant-of-allocated percent. `coverage` states orgs measured vs orgs holding IPv4 GLOBALLY (#67) - a registry with a low measured-org count is understated in this rollup, not actually low-dormancy; check coverage before ranking registries against each other.","description":"Global IPv4 dormancy totals grouped by registry (ARIN/RIPE NCC/APNIC/LACNIC/AFRINIC) - the 'which RIR has the most dormant space' rollup (#564), for a headline or a per-registry comparison rather than a per-org search (see search_dormant_ipv4 for that, and for what 'dormant' measures - the same 7-night-window definition and caveats apply here). Five rows, computed at request time from org_ipv4_utilization (already indexed on registry, no new collection). Each row reports orgs measured, allocated/advertised/dormant address totals and dormant-of-allocated percent. `coverage` states orgs measured vs orgs holding IPv4 GLOBALLY (#67) - a registry with a low measured-org count is understated in this rollup, not actually low-dormancy; check coverage before ranking registries against each other.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_growing_networks":{"post":{"operationId":"find_growing_networks","summary":"Networks ranked by how fast they are GROWING or DECLINING - net new data-centre facility presence, new IXP presence, and new address space over a 30/90/365-day window. Combine with a location (latitude+longitude+radius_km, or city/country) to answer the sales question directly: 'which fast-growing networks have a presence where I can reach them'. Set direction=declining to find networks contracting. Caveat: IXP membership history only starts 2026-04, so IXP figures are not yet a true year-over-year comparison.","description":"Networks ranked by how fast they are GROWING or DECLINING - net new data-centre facility presence, new IXP presence, and new address space over a 30/90/365-day window. Combine with a location (latitude+longitude+radius_km, or city/country) to answer the sales question directly: 'which fast-growing networks have a presence where I can reach them'. Set direction=declining to find networks contracting. Caveat: IXP membership history only starts 2026-04, so IXP figures are not yet a true year-over-year comparison.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"window_days":{"type":"integer","description":"30, 90 (default) or 365"},"direction":{"type":"string","description":"growing (default) | declining"},"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":100},"city":{"type":"string"},"country":{"type":"string","description":"ISO 2-letter code"},"presence":{"type":"string","description":"How the location is matched: 'facility' (default, PeeringDB presence) or 'ixp' (reachable on an exchange there)"},"min_facilities_added":{"type":"integer"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_growth":{"post":{"operationId":"get_asn_growth","summary":"Growth/decline detail for one ASN: net facility and IXP presence change and new address space over 30/90/365 days, with current totals for context.","description":"Growth/decline detail for one ASN: net facility and IXP presence change and new address space over 30/90/365 days, with current totals for context.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_events":{"post":{"operationId":"get_events","summary":"The change feed: ASNs newly registered, IP blocks newly allocated, resources transferred between organisations, networks joining/leaving IXPs, IXP port-speed changes, facility presence added/removed by a NETWORK, an EXCHANGE becoming present at or leaving a facility, a NETWORK gaining or losing a CDN cache, an EXCHANGE first appearing in or being delisted from PeeringDB, and a FACILITY doing the same (all #150). Filter by ASN, country, IXP, facility, event type and date range. Use for 'what changed for AS X', 'who joined IXPs in <country> recently', 'who bought address space recently', or 'what new networks appeared since <date>'. CAVEATS you must pass on: ixp_facility_added/ixp_facility_removed start EMPTY - ix_facility has no date column, so the history table took its first observation on 2026-07-27 and zero events means 'nothing has moved since we started looking', not 'no exchange has ever gained a facility'. cdn_cache_added/cdn_cache_removed start EMPTY for the same reason - cdn_cache_node is current state only, its first baseline was taken on 2026-07-27, and its granularity is the (ASN, programme) pair, so a cache farm growing from 3 nodes to 4 emits nothing. ixp_first_seen/ixp_delisted and facility_first_seen/facility_delisted are the EXCEPTION and do NOT start empty - they are derived from the monthly CAIDA PeeringDB archive and carry 2,347 events from 2010-08-01, though the series is monthly with three gaps so 111 delistings have a window up to five months wide. The facility pair adds 6,978 events on the same basis, derived from a first-seen/last-seen WINDOW rather than per-snapshot presence, which means it cannot express a gap and has no 'relisted' - measured at 6 disagreements in 1,965 when the same method is checked against exchanges. And ixp_speed_changed's 1,364 existing rows were all derived on 2026-07-25 from a membership table that has since been replaced; the current builder re-derives none of them.","description":"The change feed: ASNs newly registered, IP blocks newly allocated, resources transferred between organisations, networks joining/leaving IXPs, IXP port-speed changes, facility presence added/removed by a NETWORK, an EXCHANGE becoming present at or leaving a facility, a NETWORK gaining or losing a CDN cache, an EXCHANGE first appearing in or being delisted from PeeringDB, and a FACILITY doing the same (all #150). Filter by ASN, country, IXP, facility, event type and date range. Use for 'what changed for AS X', 'who joined IXPs in <country> recently', 'who bought address space recently', or 'what new networks appeared since <date>'. CAVEATS you must pass on: ixp_facility_added/ixp_facility_removed start EMPTY - ix_facility has no date column, so the history table took its first observation on 2026-07-27 and zero events means 'nothing has moved since we started looking', not 'no exchange has ever gained a facility'. cdn_cache_added/cdn_cache_removed start EMPTY for the same reason - cdn_cache_node is current state only, its first baseline was taken on 2026-07-27, and its granularity is the (ASN, programme) pair, so a cache farm growing from 3 nodes to 4 emits nothing. ixp_first_seen/ixp_delisted and facility_first_seen/facility_delisted are the EXCEPTION and do NOT start empty - they are derived from the monthly CAIDA PeeringDB archive and carry 2,347 events from 2010-08-01, though the series is monthly with three gaps so 111 delistings have a window up to five months wide. The facility pair adds 6,978 events on the same basis, derived from a first-seen/last-seen WINDOW rather than per-snapshot presence, which means it cannot express a gap and has no 'relisted' - measured at 6 disagreements in 1,965 when the same method is checked against exchanges. And ixp_speed_changed's 1,364 existing rows were all derived on 2026-07-25 from a membership table that has since been replaced; the current builder re-derives none of them.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"country":{"type":"string","description":"ISO 2-letter code"},"event_type":{"type":"string","description":"asn_registered | ip_allocated | resource_transferred | ixp_joined | ixp_left | ixp_speed_changed | facility_added | facility_removed | ixp_facility_added | ixp_facility_removed | cdn_cache_added | cdn_cache_removed | ixp_first_seen | ixp_delisted | facility_first_seen | facility_delisted"},"ix_id":{"type":"integer"},"facility_id":{"type":"integer"},"since":{"type":"string","description":"YYYY-MM-DD"},"until":{"type":"string","description":"YYYY-MM-DD"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_prefix_counts":{"post":{"operationId":"get_prefix_counts","summary":"Get IPv4/IPv6 prefix counts with history","description":"Get IPv4/IPv6 prefix counts with history","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_dashboard_summary":{"post":{"operationId":"get_dashboard_summary","summary":"Platform-wide traceroute corpus stats - the same three cheap /dashboard widgets (stats, recent-traces, category-breakdown), combined into one call. NOT per-user: every signed-in caller sees the same numbers, same as the web widgets they mirror. `stats` is total_traces/boomerangs/unique_asns/countries across the whole approved corpus. `recent_traces` is the 15 most recently active dest_host+dest_country pairs (NOT per-trace or per-batch - grouped by destination, distinct from get_my_traceroutes/get_asn_traces). `category_breakdown` is traceroute counts grouped by target_category (uncategorized traces are labelled 'Uncategorized', not dropped).","description":"Platform-wide traceroute corpus stats - the same three cheap /dashboard widgets (stats, recent-traces, category-breakdown), combined into one call. NOT per-user: every signed-in caller sees the same numbers, same as the web widgets they mirror. `stats` is total_traces/boomerangs/unique_asns/countries across the whole approved corpus. `recent_traces` is the 15 most recently active dest_host+dest_country pairs (NOT per-trace or per-batch - grouped by destination, distinct from get_my_traceroutes/get_asn_traces). `category_breakdown` is traceroute counts grouped by target_category (uncategorized traces are labelled 'Uncategorized', not dropped).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_boomerang_stats":{"post":{"operationId":"get_boomerang_stats","summary":"Get boomerang routing stats for a country. Returns per-ASN rates (asns, each with an attribution breakdown - avoidable/conditional/structural/unknown, #364 - or null if not yet computed for that ASN), plus by_detour_country and by_severity (regional/intercontinental/extreme/not_collected) breakdowns for the whole country - #362: a boomerang is where it went and how severe, not one blended rate. Gravity has both halves now: each ASN carries distance_gravity (avg_one_way_km, gravity_index 0-100, measured/unmeasured split per #67) and destination_gravity (top_1k/top_10k/unranked/not_collected counts from Tranco rank, #141 - coverage partial, unranked != not_collected, popularity not traffic volume), plus country-wide distance_gravity_overall/destination_gravity_overall. See destination_gravity_note in the response for the coverage and popularity-vs-traffic caveats. [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/boomerang on the site for the current caveats.]","description":"Get boomerang routing stats for a country. Returns per-ASN rates (asns, each with an attribution breakdown - avoidable/conditional/structural/unknown, #364 - or null if not yet computed for that ASN), plus by_detour_country and by_severity (regional/intercontinental/extreme/not_collected) breakdowns for the whole country - #362: a boomerang is where it went and how severe, not one blended rate. Gravity has both halves now: each ASN carries distance_gravity (avg_one_way_km, gravity_index 0-100, measured/unmeasured split per #67) and destination_gravity (top_1k/top_10k/unranked/not_collected counts from Tranco rank, #141 - coverage partial, unranked != not_collected, popularity not traffic volume), plus country-wide distance_gravity_overall/destination_gravity_overall. See destination_gravity_note in the response for the coverage and popularity-vs-traffic caveats. [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/boomerang on the site for the current caveats.]","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"}},"required":["country"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/domain_lookup":{"post":{"operationId":"domain_lookup","summary":"Two separate evidence classes for a domain, never one guessed ASN (#479). `website_associations` is a company's SELF-REPORTED website (PeeringDB/bgp.tools) - identity, not hosting. `dns_answers` is what actually resolves: observed A/AAAA records grouped by ASN from a completed domain intelligence scan, or a live DNS + routing-table resolution when the domain has never been scanned. `primary_network` is set only when dns_answers has a majority ASN, with a method/confidence/evidence field - it is null when the evidence does not support one, and it is NEVER taken from website_associations (Netflix owns AS55095 but is hosted on AWS; google.com's website row is a different ASN than the 173/174 DNS records observed for it).","description":"Two separate evidence classes for a domain, never one guessed ASN (#479). `website_associations` is a company's SELF-REPORTED website (PeeringDB/bgp.tools) - identity, not hosting. `dns_answers` is what actually resolves: observed A/AAAA records grouped by ASN from a completed domain intelligence scan, or a live DNS + routing-table resolution when the domain has never been scanned. `primary_network` is set only when dns_answers has a majority ASN, with a method/confidence/evidence field - it is null when the evidence does not support one, and it is NEVER taken from website_associations (Netflix owns AS55095 but is hosted on AWS; google.com's website row is a different ASN than the 173/174 DNS records observed for it).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"}},"required":["domain"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_traceroutes":{"post":{"operationId":"search_traceroutes","summary":"Search traceroutes by source/dest ASN, country, destination host, platform, category, boomerang, detour country, severity, address family, or quality score. NOTE `hop_count` in these rows counts RESPONDING hops, not path length, and understates any path containing a timeout - get_traceroute returns path_length_hops for a real comparison (#497). platform (ripe-atlas | linux) is the real collector/vantage-point filter - use it for \"give me RIPE Atlas traces\". category is a granular PER-TARGET label the collector assigns (241 distinct values as of 2026-08-21, e.g. \"Cross-border - US to tranco_ca\", \"ISP - TELUS Communications AS852\"), NOT a platform selector - category=\"RIPE Atlas\" only matches the literal legacy label used before the collector switched to per-target labels in April 2026 and will look empty-ish for current data (#254). category matches as a case-sensitive substring so a family like \"Cross-border\" or \"TELUS\" can be matched without knowing every exact label. Rows carry `left_source_country`, not the legacy Canada-only `leaves_canada` - a French or Brazilian domestic trace can have left_source_country=1 while never touching Canada (#363). ipv6=true restricts to af=6 traces (#424) - the RIPE Atlas importer only started writing this flag once af=6 measurements existed, so a trace older than that has ipv6=0 regardless of its real address family; a colon in dest_host/dest_ip is the fallback tell for older rows. #362: detour_country and severity filter on traceroute_boomerang_detail (migration 098) - only boomerang=1 traces ever have a row there, so passing either narrows to boomerang traces even without boomerang=true set explicitly. Rows also now carry detour_country/severity/detour_distance_km (all null on a non-boomerang row, or on a boomerang row the backfill has not yet reached).","description":"Search traceroutes by source/dest ASN, country, destination host, platform, category, boomerang, detour country, severity, address family, or quality score. NOTE `hop_count` in these rows counts RESPONDING hops, not path length, and understates any path containing a timeout - get_traceroute returns path_length_hops for a real comparison (#497). platform (ripe-atlas | linux) is the real collector/vantage-point filter - use it for \"give me RIPE Atlas traces\". category is a granular PER-TARGET label the collector assigns (241 distinct values as of 2026-08-21, e.g. \"Cross-border - US to tranco_ca\", \"ISP - TELUS Communications AS852\"), NOT a platform selector - category=\"RIPE Atlas\" only matches the literal legacy label used before the collector switched to per-target labels in April 2026 and will look empty-ish for current data (#254). category matches as a case-sensitive substring so a family like \"Cross-border\" or \"TELUS\" can be matched without knowing every exact label. Rows carry `left_source_country`, not the legacy Canada-only `leaves_canada` - a French or Brazilian domestic trace can have left_source_country=1 while never touching Canada (#363). ipv6=true restricts to af=6 traces (#424) - the RIPE Atlas importer only started writing this flag once af=6 measurements existed, so a trace older than that has ipv6=0 regardless of its real address family; a colon in dest_host/dest_ip is the fallback tell for older rows. #362: detour_country and severity filter on traceroute_boomerang_detail (migration 098) - only boomerang=1 traces ever have a row there, so passing either narrows to boomerang traces even without boomerang=true set explicitly. Rows also now carry detour_country/severity/detour_distance_km (all null on a non-boomerang row, or on a boomerang row the backfill has not yet reached).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"source_asn":{"type":"integer"},"dest_asn":{"type":"integer"},"source_country":{"type":"string"},"dest_country":{"type":"string"},"dest_host":{"type":"string"},"platform":{"type":"string","description":"ripe-atlas | linux - the collector. This, not category, is the platform discriminator."},"category":{"type":"string","description":"Substring match against the per-target label (241 distinct values). Not a platform name - see tool description."},"boomerang":{"type":"boolean"},"detour_country":{"type":"string","description":"#362: ISO2 code, matches furthest_detour_country - the single headline detour country get_boomerang_stats' by_detour_country also groups by, NOT membership in a trace's full ordered detour list (a country only transited briefly before a farther one will not match - use get_traceroute's boomerang_detail.detourCountries for the full list on one trace). Implies boomerang traces only, since a non-boomerang trace never has a detour."},"severity":{"type":"string","description":"#362: regional | intercontinental | extreme | not_collected. not_collected means boomerang=1 but detour_distance_km could not be measured (#67) - never the same as omitting this filter, which would also return every non-boomerang trace. Implies boomerang traces only."},"ipv6":{"type":"boolean","description":"true = af=6 traces only, false = af=4 traces only (#424). Omit to get both."},"min_score":{"type":"integer"},"limit":{"type":"integer","default":20}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_traceroute":{"post":{"operationId":"get_traceroute","summary":"Get one traceroute with hop geolocation provenance and a first-class domestic, left_country or undetermined crossing verdict. Legacy booleans are null when the verdict is undetermined. On path length: `hop_count` counts only hops that REPLIED and is kept for compatibility - use `path_length_hops` (the last TTL reached) to compare path lengths and `responding_hops` / `missing_hops` to see how complete the path is (#497). When boomerang=1, `boomerang_detail` (#362) carries WHERE the detour went (detour_countries, furthest_detour_country), detour_distance_km, severity_tier, severity_weight/gravity_index (a continuous 0-1 / 0-100 distance-weighted severity, same formula get_boomerang_stats aggregates - null, not 0, when detour_distance_km was not_collected, #67), destination_tier (top_1k/top_10k/unranked/null from Tranco rank - unranked means checked and not in the top 10,000, null means never checked, coverage is partial), latencyPenaltyMs (#362's fourth field - this trace's own RTT minus the median RTT of domestic, non-boomerang traces to the same destination, migration 176 - null with latencyPenaltyNotCollectedReason set to no_own_rtt/no_dest_host/insufficient_baseline/not_collected when it could not be measured; measured coverage 2026-08-25 is 126 of 699 boomerang traces, 18%, so null is the common case, never 0, #67) and #364 fault attribution - null if this trace is not a boomerang or the backfill has not reached it yet (#67). #523: each hop carries `reply_data`, per-packet Atlas responder detail from traceroute_hop_reply (migration 106) alongside the hop's own primary-responder ip/rtt_min/avg/max. `reply_data.reply_data_state` is 'observed' (with `responders`: an array, primary first, each with its own rtt_min/avg/max and reply_indexes) or 'not_collected' (`responders`: null) - NEVER read a missing/null responders as \"this hop had one responder\": only 10,122 of 3,304,540 traceroute_hop rows (0.3%, measured 2026-08-25) have any per-packet data, because it predates migration 106 for everything else and that history is unrecoverable. `reply_data.ecmp_divergence` is true only when more than one distinct responder ip was actually observed at that hop (57 such hops exist in the corpus today). Trace-level `reply_data_coverage` summarizes how many of this trace's hops have reply data at all.","description":"Get one traceroute with hop geolocation provenance and a first-class domestic, left_country or undetermined crossing verdict. Legacy booleans are null when the verdict is undetermined. On path length: `hop_count` counts only hops that REPLIED and is kept for compatibility - use `path_length_hops` (the last TTL reached) to compare path lengths and `responding_hops` / `missing_hops` to see how complete the path is (#497). When boomerang=1, `boomerang_detail` (#362) carries WHERE the detour went (detour_countries, furthest_detour_country), detour_distance_km, severity_tier, severity_weight/gravity_index (a continuous 0-1 / 0-100 distance-weighted severity, same formula get_boomerang_stats aggregates - null, not 0, when detour_distance_km was not_collected, #67), destination_tier (top_1k/top_10k/unranked/null from Tranco rank - unranked means checked and not in the top 10,000, null means never checked, coverage is partial), latencyPenaltyMs (#362's fourth field - this trace's own RTT minus the median RTT of domestic, non-boomerang traces to the same destination, migration 176 - null with latencyPenaltyNotCollectedReason set to no_own_rtt/no_dest_host/insufficient_baseline/not_collected when it could not be measured; measured coverage 2026-08-25 is 126 of 699 boomerang traces, 18%, so null is the common case, never 0, #67) and #364 fault attribution - null if this trace is not a boomerang or the backfill has not reached it yet (#67). #523: each hop carries `reply_data`, per-packet Atlas responder detail from traceroute_hop_reply (migration 106) alongside the hop's own primary-responder ip/rtt_min/avg/max. `reply_data.reply_data_state` is 'observed' (with `responders`: an array, primary first, each with its own rtt_min/avg/max and reply_indexes) or 'not_collected' (`responders`: null) - NEVER read a missing/null responders as \"this hop had one responder\": only 10,122 of 3,304,540 traceroute_hop rows (0.3%, measured 2026-08-25) have any per-packet data, because it predates migration 106 for everything else and that history is unrecoverable. `reply_data.ecmp_divergence` is true only when more than one distinct responder ip was actually observed at that hop (57 such hops exist in the corpus today). Trace-level `reply_data_coverage` summarizes how many of this trace's hops have reply data at all.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_cdn_caches":{"post":{"operationId":"search_cdn_caches","summary":"Find CDN edge caches by country/provider/city. provider: ggc | oca | fna | akamai | aws_cloudfront | microsoft | apple. Node-level LOCATIONS exist only for ggc, oca, fna, akamai, aws_cloudfront; for the rest we know which ASNs host their caches but never collected node locations (#191), so those providers return the hosting ASNs with location_data='not_collected' instead of an empty list. An empty result for them means WE DID NOT COLLECT IT, never that the caches do not exist.","description":"Find CDN edge caches by country/provider/city. provider: ggc | oca | fna | akamai | aws_cloudfront | microsoft | apple. Node-level LOCATIONS exist only for ggc, oca, fna, akamai, aws_cloudfront; for the rest we know which ASNs host their caches but never collected node locations (#191), so those providers return the hosting ASNs with location_data='not_collected' instead of an empty list. An empty result for them means WE DID NOT COLLECT IT, never that the caches do not exist.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"provider":{"type":"string","description":"ggc|oca|fna|akamai|aws_cloudfront|microsoft|apple"},"city":{"type":"string"},"limit":{"type":"integer","default":200}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/search_domains":{"post":{"operationId":"search_domains","summary":"Search the domain→ASN index (which networks host which domains). `country` filters on the hosting country OBSERVED in scan records, the same figure the coverage page counts - not the hosting network's country of registration, which is returned separately as asn_country. Only 51% of indexed domains have a scanned country, so absence from a country's results is not evidence the domain is not hosted there.","description":"Search the domain→ASN index (which networks host which domains). `country` filters on the hosting country OBSERVED in scan records, the same figure the coverage page counts - not the hosting network's country of registration, which is returned separately as asn_country. Only 51% of indexed domains have a scanned country, so absence from a country's results is not evidence the domain is not hosted there.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"asn":{"type":"integer"},"country":{"type":"string","description":"ISO 3166-1 alpha-2, e.g. DE"},"limit":{"type":"integer","default":30}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_domain_intel":{"post":{"operationId":"get_domain_intel","summary":"Domain intelligence scan summary plus a deterministic page of records. Returns total_records, returned, truncated and next_cursor. Also returns dns_profile - nameserver/mail ASN, SPF provider and DMARC - when this domain has been profiled by the #340 DNS sweep. dns_profile is null, NOT zeros, for a domain not yet profiled, and only a small share of the scanned corpus is: do not read null as \"no nameservers\". dns_profile_history_count says how many snapshots exist, because one is a baseline and cannot show a change.","description":"Domain intelligence scan summary plus a deterministic page of records. Returns total_records, returned, truncated and next_cursor. Also returns dns_profile - nameserver/mail ASN, SPF provider and DMARC - when this domain has been profiled by the #340 DNS sweep. dns_profile is null, NOT zeros, for a domain not yet profiled, and only a small share of the scanned corpus is: do not read null as \"no nameservers\". dns_profile_history_count says how many snapshots exist, because one is a baseline and cannot show a change.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"},"limit":{"type":"integer","default":100,"maximum":200},"cursor":{"type":"string"}},"required":["domain"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/trigger_domain_scan":{"post":{"operationId":"trigger_domain_scan","summary":"Queue a new Domain Intelligence scan for a domain. Requires Domain Intelligence access and an organization (subject to your org's daily/monthly scan quota). Runs asynchronously - poll get_domain_intel(domain) for the result; status starts \"queued\".","description":"Queue a new Domain Intelligence scan for a domain. Requires Domain Intelligence access and an organization (subject to your org's daily/monthly scan quota). Runs asynchronously - poll get_domain_intel(domain) for the result; status starts \"queued\".","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"}},"required":["domain"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/rerun_domain_scan":{"post":{"operationId":"rerun_domain_scan","summary":"Re-scan a domain from a previous scan you can see (yours, your org's, or any if you are an admin), linked to it via previous_scan_id. Same quota and access rules as trigger_domain_scan.","description":"Re-scan a domain from a previous scan you can see (yours, your org's, or any if you are an admin), linked to it via previous_scan_id. Same quota and access rules as trigger_domain_scan.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scan_id":{"type":"integer"}},"required":["scan_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/compare_domain_scans":{"post":{"operationId":"compare_domain_scans","summary":"Compare two Domain Intelligence scans of the same domain (subdomains, IPs, ASNs, hosting type, country). Both scans must be visible to you - yours, your org's, or any if you are an admin; an id you cannot see and one that does not exist return the same error. Returns older/newer (whichever scan_id is actually more recent by created_at), plus added/removed/changed/unchanged record lists and totalOld/totalNew counts.","description":"Compare two Domain Intelligence scans of the same domain (subdomains, IPs, ASNs, hosting type, country). Both scans must be visible to you - yours, your org's, or any if you are an admin; an id you cannot see and one that does not exist return the same error. Returns older/newer (whichever scan_id is actually more recent by created_at), plus added/removed/changed/unchanged record lists and totalOld/totalNew counts.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scan_id_1":{"type":"integer"},"scan_id_2":{"type":"integer"}},"required":["scan_id_1","scan_id_2"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_domain_scan":{"post":{"operationId":"delete_domain_scan","summary":"Delete a Domain Intelligence scan and its records. Must be a scan you can see (yours, your org's, or any if you are an admin) - same not-found error either way for a foreign or nonexistent id. This is permanent; there is no undo.","description":"Delete a Domain Intelligence scan and its records. Must be a scan you can see (yours, your org's, or any if you are an admin) - same not-found error either way for a foreign or nonexistent id. This is permanent; there is no undo.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scan_id":{"type":"integer"}},"required":["scan_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/create_export":{"post":{"operationId":"create_export","summary":"Start a reproducible asynchronous export job. Runs in the background - poll get_export(job_id) for progress; state starts \"queued\" then moves to \"running\" then a terminal state (completed/failed/cancelled). The response records the EXACT normalized filters, columns and source cutoff used, so the same call can be re-run later and compared - that is what \"reproducible\" means here. No arbitrary SQL: every dataset has a typed, allowlisted filter/column schema and an unknown filter or column is a validation error, not a silent no-op. Ten datasets ship today, and asns_search is the first one a web route serves from (POST /asns/exportSearch, #574) rather than duplicating: \"asns\" (public ASN directory), \"prefixes\" (announced routes), \"rpki_validation\" (per-prefix RPKI verdicts, joined on (asn,cidr) so withdrawn prefixes are excluded), \"ixp_members\" (one row per member, not per port), \"traceroutes\" (approved public traces only), \"facility_members\" (one row per facility membership, never grouped by resolved ASN - that would silently collapse memberships whose ASN did not resolve), \"allocations\", \"transfers\", \"events\", and \"asns_search\" (the full 52-column ASN search export, admin-only at access level 300 - the same gate the web route enforces). Call list_exports for the exact filters and columns of each. Unknown filter KEYS are rejected rather than ignored: a typo such as \"cuontry\" fails the request instead of silently exporting everything and writing the typo into the reproducibility manifest. ","description":"Start a reproducible asynchronous export job. Runs in the background - poll get_export(job_id) for progress; state starts \"queued\" then moves to \"running\" then a terminal state (completed/failed/cancelled). The response records the EXACT normalized filters, columns and source cutoff used, so the same call can be re-run later and compared - that is what \"reproducible\" means here. No arbitrary SQL: every dataset has a typed, allowlisted filter/column schema and an unknown filter or column is a validation error, not a silent no-op. Ten datasets ship today, and asns_search is the first one a web route serves from (POST /asns/exportSearch, #574) rather than duplicating: \"asns\" (public ASN directory), \"prefixes\" (announced routes), \"rpki_validation\" (per-prefix RPKI verdicts, joined on (asn,cidr) so withdrawn prefixes are excluded), \"ixp_members\" (one row per member, not per port), \"traceroutes\" (approved public traces only), \"facility_members\" (one row per facility membership, never grouped by resolved ASN - that would silently collapse memberships whose ASN did not resolve), \"allocations\", \"transfers\", \"events\", and \"asns_search\" (the full 52-column ASN search export, admin-only at access level 300 - the same gate the web route enforces). Call list_exports for the exact filters and columns of each. Unknown filter KEYS are rejected rather than ignored: a typo such as \"cuontry\" fails the request instead of silently exporting everything and writing the typo into the reproducibility manifest. ","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dataset":{"type":"string","description":"One of: asns, prefixes, rpki_validation, ixp_members, traceroutes, facility_members, allocations, transfers, events."},"filters":{"type":"object","description":"Dataset-specific filters. See the dataset's own fields; an unknown key is an error."},"columns":{"type":"array","items":{"type":"string"},"description":"Column names to include. Defaults to the dataset's default column set if omitted."},"format":{"type":"string","description":"\"csv\" (default) or \"jsonl\"."},"compression":{"type":"string","description":"\"none\" (default) or \"gzip\"."},"name":{"type":"string","description":"Optional label for your own reference, e.g. \"incident-123-evidence\"."}},"required":["dataset"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_export":{"post":{"operationId":"get_export","summary":"Status and, once complete, the manifest and download link for an export job you created. Returns three DISTINCT outcomes, never collapsed (#67): a running job has no row_count yet and is not a failure; a completed job with row_count=0 is a genuine empty result, not an error; a failed job has error_class/error_detail and is never reported as a zero-row success. download_url is present only when state=\"completed\". It is a SHORT-LIVED SIGNED link (GET /mcp/exports/:id/download?exp=&sig=) valid for 15 minutes - download_url_expires_at says exactly when - and it is minted fresh on every get_export call rather than stored, so an old response holds an expired link, not a permanent one. The signature is bound to the specific artifact, so it stops working the moment that artifact is replaced or reaped. Possession of the link is enough to download it within that window; the same path also still accepts your normal API-key/OAuth auth with no token at all, which is the safer way to fetch it if your client can set a header. A job id you do not own and one that does not exist return the identical \"Export job not found.\"","description":"Status and, once complete, the manifest and download link for an export job you created. Returns three DISTINCT outcomes, never collapsed (#67): a running job has no row_count yet and is not a failure; a completed job with row_count=0 is a genuine empty result, not an error; a failed job has error_class/error_detail and is never reported as a zero-row success. download_url is present only when state=\"completed\". It is a SHORT-LIVED SIGNED link (GET /mcp/exports/:id/download?exp=&sig=) valid for 15 minutes - download_url_expires_at says exactly when - and it is minted fresh on every get_export call rather than stored, so an old response holds an expired link, not a permanent one. The signature is bound to the specific artifact, so it stops working the moment that artifact is replaced or reaped. Possession of the link is enough to download it within that window; the same path also still accepts your normal API-key/OAuth auth with no token at all, which is the safer way to fetch it if your client can set a header. A job id you do not own and one that does not exist return the identical \"Export job not found.\"","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"integer"}},"required":["job_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/list_exports":{"post":{"operationId":"list_exports","summary":"Your own export job history (never another caller's - job visibility is scoped to the principal that created each job), newest first, paginated.","description":"Your own export job history (never another caller's - job visibility is scoped to the principal that created each job), newest first, paginated.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":20,"maximum":100},"offset":{"type":"integer","default":0}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/cancel_export":{"post":{"operationId":"cancel_export","summary":"Cancel a queued or running export job you created. A job already in a terminal state (completed/failed/cancelled/expired) returns an explicit \"already <state>\" refusal rather than a silent no-op success. Cancellation here is LOCAL (this server's in-process runner checks for it between batches and cleans up the partial file) - there is no separate remote worker to signal, which the response's note field says explicitly.","description":"Cancel a queued or running export job you created. A job already in a terminal state (completed/failed/cancelled/expired) returns an explicit \"already <state>\" refusal rather than a silent no-op success. Cancellation here is LOCAL (this server's in-process runner checks for it between batches and cleans up the partial file) - there is no separate remote worker to signal, which the response's note field says explicitly.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"integer"}},"required":["job_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/create_measurement":{"post":{"operationId":"create_measurement","summary":"Launch an on-demand ping, traceroute, or dns lookup from a Globalping vantage point (#552). Returns immediately with state=\"queued\" - this does NOT wait for results; poll get_measurement(job_id). Requests are NEVER deduplicated against each other or against scheduled coverage: every call creates a fresh measurement, on purpose, so an incident-response retry always gets a real new probe. Pass idempotency_key to make a RETRY of the same logical request safe instead - a second call with the same key returns the original request (dedup=\"reused_idempotency_key\") rather than spending quota twice. Guardrails, all enforced before submission: for ping/traceroute, the target is resolved and rejected if it (or any of its resolved addresses) falls in a private/loopback/link-local/multicast/reserved/cloud-metadata range; for dns, the target (the domain QUERIED) is only shape-checked - it is deliberately NOT pre-resolved/blocked, because a name that legitimately NXDOMAINs or has no A/AAAA record is a normal, valid dns target, not an error (#67) - but dns_resolver (see below), a real network endpoint the probe connects to, gets the SAME private-range rejection target does for ping/traceroute; a per-caller cap of 20 probes per rolling hour and at most 3 concurrent in-progress requests, both independent of Globalping's own platform-wide free allowance (one caller cannot exhaust that allowance for everyone else); budget_mode is fixed to \"free_only\" (credit spending is not available yet - passing \"credits_allowed\" is refused). address_families defaults to [4]; passing [4,6] submits one job per family (paired v4/v6), sharing one job_id/request group. probes_per_selector is capped at 5. For type=\"dns\": the result carries a `dns_outcome` per answer - \"resolved\" (NOERROR, with `answers`/`resolved_addresses`), \"nxdomain\" (a real completed answer, not an error), or \"dns_error\" (SERVFAIL/REFUSED/etc, code preserved) - alongside the usual transport-level `status`, which is what actually failing to get any DNS response (timeout, resolver unreachable) reports; \"not yet run\" is the job/request state, polled via get_measurement. source REQUIRES at least one of countries[0]/asns[0]/cities[0] (a single Globalping location selector; there is no \"any probe worldwide\" option in this slice) plus any number of tags to narrow it - selecting several DISCRETE locations in one call (e.g. \"3 different ISPs\") is not built in this slice.","description":"Launch an on-demand ping, traceroute, or dns lookup from a Globalping vantage point (#552). Returns immediately with state=\"queued\" - this does NOT wait for results; poll get_measurement(job_id). Requests are NEVER deduplicated against each other or against scheduled coverage: every call creates a fresh measurement, on purpose, so an incident-response retry always gets a real new probe. Pass idempotency_key to make a RETRY of the same logical request safe instead - a second call with the same key returns the original request (dedup=\"reused_idempotency_key\") rather than spending quota twice. Guardrails, all enforced before submission: for ping/traceroute, the target is resolved and rejected if it (or any of its resolved addresses) falls in a private/loopback/link-local/multicast/reserved/cloud-metadata range; for dns, the target (the domain QUERIED) is only shape-checked - it is deliberately NOT pre-resolved/blocked, because a name that legitimately NXDOMAINs or has no A/AAAA record is a normal, valid dns target, not an error (#67) - but dns_resolver (see below), a real network endpoint the probe connects to, gets the SAME private-range rejection target does for ping/traceroute; a per-caller cap of 20 probes per rolling hour and at most 3 concurrent in-progress requests, both independent of Globalping's own platform-wide free allowance (one caller cannot exhaust that allowance for everyone else); budget_mode is fixed to \"free_only\" (credit spending is not available yet - passing \"credits_allowed\" is refused). address_families defaults to [4]; passing [4,6] submits one job per family (paired v4/v6), sharing one job_id/request group. probes_per_selector is capped at 5. For type=\"dns\": the result carries a `dns_outcome` per answer - \"resolved\" (NOERROR, with `answers`/`resolved_addresses`), \"nxdomain\" (a real completed answer, not an error), or \"dns_error\" (SERVFAIL/REFUSED/etc, code preserved) - alongside the usual transport-level `status`, which is what actually failing to get any DNS response (timeout, resolver unreachable) reports; \"not yet run\" is the job/request state, polled via get_measurement. source REQUIRES at least one of countries[0]/asns[0]/cities[0] (a single Globalping location selector; there is no \"any probe worldwide\" option in this slice) plus any number of tags to narrow it - selecting several DISCRETE locations in one call (e.g. \"3 different ISPs\") is not built in this slice.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"\"ping\", \"traceroute\", or \"dns\" - tls/ntp are not available in this slice"},"target":{"type":"string","description":"A bare hostname or IP address - not a URL. Rejected if it is (or resolves to) a private/loopback/link-local/multicast/reserved/cloud-metadata address."},"address_families":{"type":"array","items":{"type":"integer"},"description":"[4], [6], or [4,6] for a paired run. Default [4]."},"source":{"type":"object","description":"REQUIRED: at least one of countries[0]/asns[0]/cities[0], plus optional tags[].","properties":{"countries":{"type":"array","items":{"type":"string"},"description":"At most one ISO 3166-1 alpha-2 code."},"asns":{"type":"array","items":{"type":"integer"},"description":"At most one."},"cities":{"type":"array","items":{"type":"string"},"description":"At most one."},"tags":{"type":"array","items":{"type":"string"}}}},"probes_per_selector":{"type":"integer","default":1,"description":"Probes per address family, max 5."},"dns_query_type":{"type":"string","description":"type=\"dns\" only. DNS record type to query: A, AAAA, ANY, CNAME, DNSKEY, DS, HTTPS, MX, NS, NSEC, PTR, RRSIG, SOA, TXT, SRV, or SVCB. Default \"A\"."},"dns_resolver":{"type":"string","description":"type=\"dns\" only. Optional custom resolver hostname or IP to query instead of the probe's own system resolver. Rejected if it is (or resolves to) a private/loopback/link-local/multicast/reserved/cloud-metadata address - same rule as `target` for ping/traceroute."},"provider":{"type":"string","description":"Only \"globalping\" is available in this slice."},"budget_mode":{"type":"string","description":"Only \"free_only\" is available in this slice."},"idempotency_key":{"type":"string","description":"Optional. A retry with the same key returns the original request instead of creating a new one."},"description":{"type":"string","description":"Optional free-text label for your own reference, e.g. \"incident-123\"."}},"required":["type","target","source"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_measurement":{"post":{"operationId":"get_measurement","summary":"Status and results for a measurement job you created (#552). Three states, never collapsed (#67): \"running\" (still in progress at the provider), a terminal state with usable_result_count=0 / zero_usable_results=true (the job ran to completion but nothing came back \"finished\" - a real distinct outcome, not an error and not silently treated as success), and \"failed\" (a real submission/provider error, in `error` and per-job `last_error`). \"partial\" appears when a paired v4/v6 request had one address family succeed and the other fail or get cancelled. Every job (one per requested address family) reports its own state/attempts/last_error alongside the request-level rollup, and every result row keeps the RAW provider payload (`raw`) as well as a normalized `summary` - never a single collapsed value, so multiple per-hop responders are never reported as loss (#523). A job id you do not own and one that does not exist return the identical \"Measurement job not found.\"","description":"Status and results for a measurement job you created (#552). Three states, never collapsed (#67): \"running\" (still in progress at the provider), a terminal state with usable_result_count=0 / zero_usable_results=true (the job ran to completion but nothing came back \"finished\" - a real distinct outcome, not an error and not silently treated as success), and \"failed\" (a real submission/provider error, in `error` and per-job `last_error`). \"partial\" appears when a paired v4/v6 request had one address family succeed and the other fail or get cancelled. Every job (one per requested address family) reports its own state/attempts/last_error alongside the request-level rollup, and every result row keeps the RAW provider payload (`raw`) as well as a normalized `summary` - never a single collapsed value, so multiple per-hop responders are never reported as loss (#523). A job id you do not own and one that does not exist return the identical \"Measurement job not found.\"","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"integer"}},"required":["job_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/list_measurements":{"post":{"operationId":"list_measurements","summary":"List the measurement jobs you created (#552), newest first. Scoped to you: another user's jobs are never returned, and a non-admin passing someone else's user_id gets the same \"Measurement job not found.\" that get_measurement uses, so this cannot probe which job ids exist. Filters: state, measurement_type, target (exact match, not a substring), created_after / created_before. limit defaults to 25, clamped to 100. Each row carries the same rolled-up lifecycle state get_measurement reports, from one shared function, so a list can never disagree with a detail view.","description":"List the measurement jobs you created (#552), newest first. Scoped to you: another user's jobs are never returned, and a non-admin passing someone else's user_id gets the same \"Measurement job not found.\" that get_measurement uses, so this cannot probe which job ids exist. Filters: state, measurement_type, target (exact match, not a substring), created_after / created_before. limit defaults to 25, clamped to 100. Each row carries the same rolled-up lifecycle state get_measurement reports, from one shared function, so a list can never disagree with a detail view.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"state":{"type":"string"},"measurement_type":{"type":"string"},"target":{"type":"string"},"created_after":{"type":"string"},"created_before":{"type":"string"},"limit":{"type":"integer","default":25},"offset":{"type":"integer","default":0}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/cancel_measurement":{"post":{"operationId":"cancel_measurement","summary":"Cancel a measurement job you created (#552). Local and provider outcomes are reported SEPARATELY and an upstream refusal is never reported as a successful cancel: a job not yet submitted to Globalping (still claimed in the ledger only) is genuinely cancelled and quota is never spent on it; a job already submitted cannot be cancelled - Globalping has no measurement-cancellation API - and the response says so explicitly (`provider_cancelled` is always false; `jobs_already_submitted_to_provider` counts what could not be stopped) rather than pretending the request went away. A job already in a terminal state (complete/failed/cancelled) returns that state with an explanatory note, not an error.","description":"Cancel a measurement job you created (#552). Local and provider outcomes are reported SEPARATELY and an upstream refusal is never reported as a successful cancel: a job not yet submitted to Globalping (still claimed in the ledger only) is genuinely cancelled and quota is never spent on it; a job already submitted cannot be cancelled - Globalping has no measurement-cancellation API - and the response says so explicitly (`provider_cancelled` is always false; `jobs_already_submitted_to_provider` counts what could not be stopped) rather than pretending the request went away. A job already in a terminal state (complete/failed/cancelled) returns that state with an explanatory note, not an error.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"job_id":{"type":"integer"}},"required":["job_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_density":{"post":{"operationId":"get_asn_density","summary":"The per-location IP-density breakdown behind the prospect map for one ASN. Requires Sales access. Locations are geolocated address-space observations, not registered offices or facilities.","description":"The per-location IP-density breakdown behind the prospect map for one ASN. Requires Sales access. Locations are geolocated address-space observations, not registered offices or facilities.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/compare_asns":{"post":{"operationId":"compare_asns","summary":"Compare multiple ASNs side by side - PoPs, IXPs, prefixes, routing quality, announced IPv4 address total, BGP downstream count and the owning registry organisation. Same columns as the /compare page. CAVEATS: `ipv4_addresses` is the merged union of announced space (more-specifics collapsed), and null means the network announces no IPv4 - it is not zero. `bgp_downstream_networks` comes from AS_PATH adjacency, which does NOT label relationships: an adjacency may be transit OR peering, and it is not proven hierarchy. It is a FLOOR, not a census - a RIB holds only the best path per prefix per collector peer, so adjacencies that are never anyone's best path (typical of settlement-free IXP peering) are missing regardless of collector count (#208) - and it is null, not 0, when this ASN was never observed adjacent to anything by any configured collector (#67). `org` is resolved through the RIR allocation record, not asn.org_id. `org.org_url` is the canonical browser link for that organisation (models/org-slug.js), null when it could not be resolved; it replaces the old `org.url`, which was the /orgs/{registry}/{org_key} fallback form rather than the canonical slug. `boomerang_rate` and `crossing_rate` are DIFFERENT measurements and are returned separately: crossing_rate is traces that left the country, boomerang_rate is traces that left and came back (#126, #485). `pop_count` and `ixp_count` are null, never 0, when PeeringDB has no record or an empty one, with the reason in `pop_count_note` / `ixp_count_note` (#496). Every ASN you ask for comes back in the order you asked, and one we hold no row for carries an `error` field rather than being dropped (#498).","description":"Compare multiple ASNs side by side - PoPs, IXPs, prefixes, routing quality, announced IPv4 address total, BGP downstream count and the owning registry organisation. Same columns as the /compare page. CAVEATS: `ipv4_addresses` is the merged union of announced space (more-specifics collapsed), and null means the network announces no IPv4 - it is not zero. `bgp_downstream_networks` comes from AS_PATH adjacency, which does NOT label relationships: an adjacency may be transit OR peering, and it is not proven hierarchy. It is a FLOOR, not a census - a RIB holds only the best path per prefix per collector peer, so adjacencies that are never anyone's best path (typical of settlement-free IXP peering) are missing regardless of collector count (#208) - and it is null, not 0, when this ASN was never observed adjacent to anything by any configured collector (#67). `org` is resolved through the RIR allocation record, not asn.org_id. `org.org_url` is the canonical browser link for that organisation (models/org-slug.js), null when it could not be resolved; it replaces the old `org.url`, which was the /orgs/{registry}/{org_key} fallback form rather than the canonical slug. `boomerang_rate` and `crossing_rate` are DIFFERENT measurements and are returned separately: crossing_rate is traces that left the country, boomerang_rate is traces that left and came back (#126, #485). `pop_count` and `ixp_count` are null, never 0, when PeeringDB has no record or an empty one, with the reason in `pop_count_note` / `ixp_count_note` (#496). Every ASN you ask for comes back in the order you asked, and one we hold no row for carries an `error` field rather than being dropped (#498).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asns":{"type":"array","items":{"type":"integer"}}},"required":["asns"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_history":{"post":{"operationId":"get_asn_history","summary":"Historical trends for an ASN: facility/IXP counts, total IXP capacity, and prefix counts over time","description":"Historical trends for an ASN: facility/IXP counts, total IXP capacity, and prefix counts over time","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_route_history":{"post":{"operationId":"get_route_history","summary":"Historical view of an ASN's observed adjacency set (monthly, since 2001-10-26) or a prefix's origin-set changes (daily, since the series started - no backfill). THREE states throughout and they must not be collapsed: `gap` (no observation covering that period - we did not look), observed-absent (we looked and it was not there), observed-present. A gap is NOT a withdrawal. Prefix history before `series_start` is `unknown`, never 'not announced'. AS_PATH change points are NOT available: raw MRT is not retained, so `path_history` returns not_available rather than an empty array.","description":"Historical view of an ASN's observed adjacency set (monthly, since 2001-10-26) or a prefix's origin-set changes (daily, since the series started - no backfill). THREE states throughout and they must not be collapsed: `gap` (no observation covering that period - we did not look), observed-absent (we looked and it was not there), observed-present. A gap is NOT a withdrawal. Prefix history before `series_start` is `unknown`, never 'not announced'. AS_PATH change points are NOT available: raw MRT is not retained, so `path_history` returns not_available rather than an empty array.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"resource":{"type":"string","description":"AS64496, a CIDR like 192.0.2.0/24, or a bare IPv4/IPv6 address"},"from":{"type":"string","description":"ISO date (YYYY-MM or YYYY-MM-DD), optional"},"to":{"type":"string","description":"ISO date, optional"}},"required":["resource"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_bgp_state":{"post":{"operationId":"get_bgp_state","summary":"Current visible origin-set for an ASN, a prefix or an IP address, from the platform's nightly internal snapshot - NOT a live query (this platform has no live BGP feed access). Every response carries a `snapshot` block stating freshness (fresh/stale/unavailable) and provenance, read at request time. MOAS-aware: a prefix's `origins` array is never reduced to one row. `path_observation` is always `not_available` (raw AS_PATH is never retained). `at` (an ISO date) replays a prefix or IP's origin-set history to that date and is NOT supported for an ASN resource (returns a structured refusal, not approximated current data). `limit`/`cursor` paginate an ASN's prefix list.","description":"Current visible origin-set for an ASN, a prefix or an IP address, from the platform's nightly internal snapshot - NOT a live query (this platform has no live BGP feed access). Every response carries a `snapshot` block stating freshness (fresh/stale/unavailable) and provenance, read at request time. MOAS-aware: a prefix's `origins` array is never reduced to one row. `path_observation` is always `not_available` (raw AS_PATH is never retained). `at` (an ISO date) replays a prefix or IP's origin-set history to that date and is NOT supported for an ASN resource (returns a structured refusal, not approximated current data). `limit`/`cursor` paginate an ASN's prefix list.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"resource":{"type":"string","description":"AS64496, a CIDR like 192.0.2.0/24, or a bare IPv4/IPv6 address"},"at":{"type":"string","description":"ISO date (YYYY-MM-DD), optional - point-in-time replay, prefix/IP resources only"},"limit":{"type":"integer","description":"Max prefixes to return for an ASN resource, default 100, max 500"},"cursor":{"type":"string","description":"Opaque pagination cursor from a prior response’s pagination.next_cursor, ASN resource only"}},"required":["resource"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_neighbours":{"post":{"operationId":"get_asn_neighbours","summary":"Every ASN observed adjacent to this one in BGP AS_PATHs, in BOTH directions - this is the tool for \"who does X peer with\". CAVEATS: AS_PATH does NOT label relationships, so an adjacency may be transit OR settlement-free peering OR a sibling; it is not proof of a commercial arrangement. Direction is \"seen nearer/further from the origin\", an observation and not proven hierarchy. Built from a curated multi-collector RouteViews + RIPE RIS set; the response's observed_from field names the exact collectors and the build date, so do not describe the coverage from memory. A sparse result is mostly a statement about collector visibility, not about the network, and it stays that way at any collector count: a RIB carries only the best path per prefix per collector peer, so IXP route-server peering is largely invisible here (#208). For networks that merely share an exchange - opportunity rather than observation - use get_asn_ixp_colocated instead.","description":"Every ASN observed adjacent to this one in BGP AS_PATHs, in BOTH directions - this is the tool for \"who does X peer with\". CAVEATS: AS_PATH does NOT label relationships, so an adjacency may be transit OR settlement-free peering OR a sibling; it is not proof of a commercial arrangement. Direction is \"seen nearer/further from the origin\", an observation and not proven hierarchy. Built from a curated multi-collector RouteViews + RIPE RIS set; the response's observed_from field names the exact collectors and the build date, so do not describe the coverage from memory. A sparse result is mostly a statement about collector visibility, not about the network, and it stays that way at any collector count: a RIB carries only the best path per prefix per collector peer, so IXP route-server peering is largely invisible here (#208). For networks that merely share an exchange - opportunity rather than observation - use get_asn_ixp_colocated instead.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"limit":{"type":"integer","default":100}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_ixp_colocated":{"post":{"operationId":"get_asn_ixp_colocated","summary":"Networks present on at least one of the same internet exchanges as this ASN, from PeeringDB membership, ranked by shared exchanges. NOT A PEER LIST: co-presence on a fabric is a peering OPPORTUNITY, and two networks can share an exchange and exchange no traffic. Covers only ASNs with a PeeringDB record and a listed port. For neighbours actually observed in BGP, use get_asn_neighbours.","description":"Networks present on at least one of the same internet exchanges as this ASN, from PeeringDB membership, ranked by shared exchanges. NOT A PEER LIST: co-presence on a fabric is a peering OPPORTUNITY, and two networks can share an exchange and exchange no traffic. Covers only ASNs with a PeeringDB record and a listed port. For neighbours actually observed in BGP, use get_asn_neighbours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"limit":{"type":"integer","default":50}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_peers":{"post":{"operationId":"get_asn_peers","summary":"DEPRECATED ALIAS for get_asn_ixp_colocated - despite the name it returns IXP co-presence, not peers. Prefer get_asn_neighbours for observed BGP adjacency, or get_asn_ixp_colocated for shared exchanges.","description":"DEPRECATED ALIAS for get_asn_ixp_colocated - despite the name it returns IXP co-presence, not peers. Prefer get_asn_neighbours for observed BGP adjacency, or get_asn_ixp_colocated for shared exchanges.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"limit":{"type":"integer","default":50}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_lead_score":{"post":{"operationId":"get_asn_lead_score","summary":"Sales lead score and opportunity signals for an ASN","description":"Sales lead score and opportunity signals for an ASN","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_route_heatmap":{"post":{"operationId":"get_route_heatmap","summary":"Busiest city-to-city links observed in traceroute data (internet route heatmap). Pass from_city (and optionally from_country, to disambiguate a repeated city name) to answer a different question - not \"what are the busiest links overall\" but \"where do paths go NEXT from this specific city\" (#350) - the traceroutes.js /analysis/city-links gap this closes. from_city matches the merged/normalized spelling this tool already returns in from_city/to_city, not raw traceroute_hop.city.","description":"Busiest city-to-city links observed in traceroute data (internet route heatmap). Pass from_city (and optionally from_country, to disambiguate a repeated city name) to answer a different question - not \"what are the busiest links overall\" but \"where do paths go NEXT from this specific city\" (#350) - the traceroutes.js /analysis/city-links gap this closes. from_city matches the merged/normalized spelling this tool already returns in from_city/to_city, not raw traceroute_hop.city.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"from_city":{"type":"string","description":"Restrict to links leaving this city - answers \"where do paths go next from here\" (#350)."},"from_country":{"type":"string","description":"Disambiguates from_city when the same city name exists in more than one country."},"limit":{"type":"integer","default":40}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_country_scoreboard":{"post":{"operationId":"get_country_scoreboard","summary":"Countries ranked by routing quality (boomerang rate, cross-border rate) [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/country-leaderboard on the site for the current caveats.]","description":"Countries ranked by routing quality (boomerang rate, cross-border rate) [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/country-leaderboard on the site for the current caveats.]","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_tier1_comparison":{"post":{"operationId":"get_tier1_comparison","summary":"The 11 curated Tier-1 transit providers, ranked by BGP-wide downstream network count (RIB-derived, so the order does not depend on where our probes are). Reads the same durable connectivity_snapshot_run/connectivity_asn_metric/interconnect_provider snapshot as the /traceroutes/analysis/tier1 page (models/tier1-snapshot.js) rather than running its own live query, so this tool and that page cannot disagree about the same named comparison (#528, one level up from the fix #78/#438 already made for population/window/ranking here). transit_traces, avg_rtt, avg_hops etc are all frozen at the snapshot's own window - see the returned snapshot.window_start/window_end, not a rolling \"as of now\" figure. tier1_peers_observed and cloud_interconnects_observed are TIER1<->TIER1 and TIER1<->cloud/CDN handoff EVIDENCE from that same snapshot; the old customer_networks/handoff_cities/observations figures (pulled live from a separate asn_transit_handoffs table on its own 180-day window, never shown on the page at all) are retired by this migration - see notes.retired. Returns available:false with an empty providers list if no connectivity snapshot has completed yet. Traceroute counts are corpus floors, not traffic shares.","description":"The 11 curated Tier-1 transit providers, ranked by BGP-wide downstream network count (RIB-derived, so the order does not depend on where our probes are). Reads the same durable connectivity_snapshot_run/connectivity_asn_metric/interconnect_provider snapshot as the /traceroutes/analysis/tier1 page (models/tier1-snapshot.js) rather than running its own live query, so this tool and that page cannot disagree about the same named comparison (#528, one level up from the fix #78/#438 already made for population/window/ranking here). transit_traces, avg_rtt, avg_hops etc are all frozen at the snapshot's own window - see the returned snapshot.window_start/window_end, not a rolling \"as of now\" figure. tier1_peers_observed and cloud_interconnects_observed are TIER1<->TIER1 and TIER1<->cloud/CDN handoff EVIDENCE from that same snapshot; the old customer_networks/handoff_cities/observations figures (pulled live from a separate asn_transit_handoffs table on its own 180-day window, never shown on the page at all) are retired by this migration - see notes.retired. Returns available:false with an empty providers list if no connectivity snapshot has completed yet. Traceroute counts are corpus floors, not traffic shares.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"snapshot_id":{"type":"integer","description":"Optional: a specific historical completed snapshot ID instead of the latest (same IDs as get_asn_connectivity_profile and /traceroutes/analysis/tier1?snapshot_id=)."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_tier1_city_view":{"post":{"operationId":"get_tier1_city_view","summary":"Tier 1 x Tier 1 peering and Tier 1 x cloud handoffs actually observed in traceroutes through one city or metro area, over the snapshot's window - the server-side equivalent of the By City section on /traceroutes/analysis/tier1. Reads the same durable connectivity snapshot as get_tier1_comparison (models/tier1-snapshot.js's getTier1Snapshot()) rather than a live query, so this tool and that page cannot disagree. A city with nothing here means this corpus has no probe traffic through it, NOT that no Tier 1 is present there (#67) - check get_tier1_comparison for what the corpus does cover. `city` is matched as a case-insensitive substring against the snapshot's own city names, same as the page's search box.","description":"Tier 1 x Tier 1 peering and Tier 1 x cloud handoffs actually observed in traceroutes through one city or metro area, over the snapshot's window - the server-side equivalent of the By City section on /traceroutes/analysis/tier1. Reads the same durable connectivity snapshot as get_tier1_comparison (models/tier1-snapshot.js's getTier1Snapshot()) rather than a live query, so this tool and that page cannot disagree. A city with nothing here means this corpus has no probe traffic through it, NOT that no Tier 1 is present there (#67) - check get_tier1_comparison for what the corpus does cover. `city` is matched as a case-insensitive substring against the snapshot's own city names, same as the page's search box.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"city":{"type":"string","description":"City or metro name, case-insensitive substring."},"snapshot_id":{"type":"integer","description":"Optional: a specific completed snapshot instead of the latest."}},"required":["city"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_connectivity_profile":{"post":{"operationId":"get_asn_connectivity_profile","summary":"Precomputed connectivity profile for ANY ASN, not just the curated Tier-1 eleven (#528 generalises #527's Tier-1 snapshot into a per-ASN model). Reads only a durable, versioned snapshot - the same connectivity_snapshot_run/connectivity_asn_metric/connectivity_observed_handoff/interconnect_provider tables get_tier1_comparison also reads - and never falls back to a live aggregate, so a stale or failed refresh is visible as staleness rather than silently substituted with a different computation. `measured` means this ASN was in the precomputed population for that snapshot with real observations; `unobserved` means it was not selected or had no evidence - absence is not zero (#67), never report an unobserved ASN as having no connectivity. observed_handoffs are consecutive-ASN-transition EVIDENCE, not a commercial relationship claim (transit vs settlement-free peering vs sibling cannot be told apart from adjacency alone). This profile does not read asn_adjacency/PeeringDB directly, so it does not carry the collector geographic bias documented on get_asn_neighbours/#208 - but that also means it currently has NO population beyond whatever the curated seed list covers until #528's broader population and priority-score work lands. If available is false, no completed snapshot exists (or the requested snapshot_id does not); that is a fault on our side, not evidence about the ASN. Three further sections share this same snapshot, each with its own three-state `state` (measured / measured_none_observed / unobserved) and `note` exactly like the top-level fields above - measured_none_observed means this ASN WAS measured and simply had no such evidence in this window, never collapse that with unobserved (not in the precomputed population at all): `cdn_cloud_interconnects` (observed handoffs to a curated cloud/CDN interconnect_provider ASN, canonical multi-ASN operators like AWS/Google/Microsoft merged into one entry); `metro_path_diversity` (distinct metros with observed handoff evidence - metro BREADTH only, never a redundancy claim); and `observed_path_diversity` (#528 section 3, per-handoff interface/IXP/temporal-path evidence from connectivity_handoff_interface/connectivity_handoff_path - labelled Observed Path Diversity, never Redundancy, since traceroutes cannot prove contractual, physical or operational redundancy. Two tiers kept separate: WEAK is distinct interface IPs, which may be two routers or two interfaces on ONE router since alias resolution is not collected; STRONG is an IXP actually observed on the crossing, sparse by measurement (only 20 of 500 handoff rows with interface data, 2026-08-25/26). Concurrency is REQUIRED, not assumed - a handoff's `paths.concurrent_overlapping_pairs` only counts path pairs whose own first_seen/last_seen windows actually overlap; a path used last year and a replacement used today do not count. connectivity_handoff_path is CURRENTLY EMPTY for the whole snapshot - every handoff reads `paths.state: 'no_path_rows'` right now, which is a snapshot-build gap, not a finding of 'one path' or 'no diversity').","description":"Precomputed connectivity profile for ANY ASN, not just the curated Tier-1 eleven (#528 generalises #527's Tier-1 snapshot into a per-ASN model). Reads only a durable, versioned snapshot - the same connectivity_snapshot_run/connectivity_asn_metric/connectivity_observed_handoff/interconnect_provider tables get_tier1_comparison also reads - and never falls back to a live aggregate, so a stale or failed refresh is visible as staleness rather than silently substituted with a different computation. `measured` means this ASN was in the precomputed population for that snapshot with real observations; `unobserved` means it was not selected or had no evidence - absence is not zero (#67), never report an unobserved ASN as having no connectivity. observed_handoffs are consecutive-ASN-transition EVIDENCE, not a commercial relationship claim (transit vs settlement-free peering vs sibling cannot be told apart from adjacency alone). This profile does not read asn_adjacency/PeeringDB directly, so it does not carry the collector geographic bias documented on get_asn_neighbours/#208 - but that also means it currently has NO population beyond whatever the curated seed list covers until #528's broader population and priority-score work lands. If available is false, no completed snapshot exists (or the requested snapshot_id does not); that is a fault on our side, not evidence about the ASN. Three further sections share this same snapshot, each with its own three-state `state` (measured / measured_none_observed / unobserved) and `note` exactly like the top-level fields above - measured_none_observed means this ASN WAS measured and simply had no such evidence in this window, never collapse that with unobserved (not in the precomputed population at all): `cdn_cloud_interconnects` (observed handoffs to a curated cloud/CDN interconnect_provider ASN, canonical multi-ASN operators like AWS/Google/Microsoft merged into one entry); `metro_path_diversity` (distinct metros with observed handoff evidence - metro BREADTH only, never a redundancy claim); and `observed_path_diversity` (#528 section 3, per-handoff interface/IXP/temporal-path evidence from connectivity_handoff_interface/connectivity_handoff_path - labelled Observed Path Diversity, never Redundancy, since traceroutes cannot prove contractual, physical or operational redundancy. Two tiers kept separate: WEAK is distinct interface IPs, which may be two routers or two interfaces on ONE router since alias resolution is not collected; STRONG is an IXP actually observed on the crossing, sparse by measurement (only 20 of 500 handoff rows with interface data, 2026-08-25/26). Concurrency is REQUIRED, not assumed - a handoff's `paths.concurrent_overlapping_pairs` only counts path pairs whose own first_seen/last_seen windows actually overlap; a path used last year and a replacement used today do not count. connectivity_handoff_path is CURRENTLY EMPTY for the whole snapshot - every handoff reads `paths.state: 'no_path_rows'` right now, which is a snapshot-build gap, not a finding of 'one path' or 'no diversity').","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"Any public ASN, not limited to the curated Tier-1 set."},"snapshot_id":{"type":"integer","description":"Optional: a specific historical snapshot ID from get_tier1_comparison-style history. Defaults to the latest completed snapshot."}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_common_routes":{"post":{"operationId":"get_common_routes","summary":"Most frequently observed source-ASN → destination-ASN traffic flows [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/routes on the site for the current caveats.]","description":"Most frequently observed source-ASN → destination-ASN traffic flows [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/routes on the site for the current caveats.]","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"limit":{"type":"integer","default":40}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_routing_quality":{"post":{"operationId":"get_asn_routing_quality","summary":"ASNs ranked by traceroute-observed routing quality (boomerang rate) among traces THEY SOURCED in the last 180 days - existing evidence, not a peering candidate suggestion (see get_recommended_peers for that). Same DOMESTIC_TRACE eligibility floor as get_country_scoreboard (#130/#179): only traces confirmed domestic-or-crossing count toward eligible_traces, never the raw total. classification is needs-more-data (below 200 eligible traces), poor-peering (>30% boomerang), moderate (>10%), or good - the SAME floor-then-threshold order the web page uses so colour/label cannot disagree (#249). Admin only (access_level >= 300); the web page also accepts a personal beta_features flag below that level, which this tool does not replicate.","description":"ASNs ranked by traceroute-observed routing quality (boomerang rate) among traces THEY SOURCED in the last 180 days - existing evidence, not a peering candidate suggestion (see get_recommended_peers for that). Same DOMESTIC_TRACE eligibility floor as get_country_scoreboard (#130/#179): only traces confirmed domestic-or-crossing count toward eligible_traces, never the raw total. classification is needs-more-data (below 200 eligible traces), poor-peering (>30% boomerang), moderate (>10%), or good - the SAME floor-then-threshold order the web page uses so colour/label cannot disagree (#249). Admin only (access_level >= 300); the web page also accepts a personal beta_features flag below that level, which this tool does not replicate.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string","description":"Filters on the OBSERVED source_country the trace launched from, not the ASN registry country."},"search":{"type":"string","description":"Matches source ASN number or name."},"limit":{"type":"integer","default":50,"maximum":200}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_prospects":{"post":{"operationId":"find_prospects","summary":"Find sales-prospect networks by use case. use_case: transit | colocation | ixp | ixp_competitive | network_health. Same engine as the web prospecting tool. Dismissed/tagged leads are hidden by default, matching /prospect; set hide_dismissed=false to include them. target_ixp changes what use_case=ixp MEANS: without it you get networks on no domestic exchange at all, with it you get candidates for THAT exchange - anyone already peering there on a working port is excluded, and the rest are ranked by how many of the exchange's own facilities they are already present in, because a network in the building can be cross-connected and one three cities away cannot (#504). An unknown target_ixp is an error, not a silent fallback. `country` (the PROSPECT network's registered country, e.g. \"US\" or \"BR\") is REQUIRED and has no default - it used to silently fall back to CA (#363), so an omitted country now returns an error instead of a Canadian answer to a non-Canadian question. For use_case=transit specifically, `country` filters the prospect's own registry, not the probe location (it used to conflate the two and return networks from other countries, #491); `source_country` filters where the traces were measured FROM, if you want that instead. crossing_rate is computed from left_source_country, the general column, not the legacy Canada-only leaves_canada - for CA that is 34,406 crossings rather than 10,093, so rankings and the 10% inclusion threshold differ from before.","description":"Find sales-prospect networks by use case. use_case: transit | colocation | ixp | ixp_competitive | network_health. Same engine as the web prospecting tool. Dismissed/tagged leads are hidden by default, matching /prospect; set hide_dismissed=false to include them. target_ixp changes what use_case=ixp MEANS: without it you get networks on no domestic exchange at all, with it you get candidates for THAT exchange - anyone already peering there on a working port is excluded, and the rest are ranked by how many of the exchange's own facilities they are already present in, because a network in the building can be cross-connected and one three cities away cannot (#504). An unknown target_ixp is an error, not a silent fallback. `country` (the PROSPECT network's registered country, e.g. \"US\" or \"BR\") is REQUIRED and has no default - it used to silently fall back to CA (#363), so an omitted country now returns an error instead of a Canadian answer to a non-Canadian question. For use_case=transit specifically, `country` filters the prospect's own registry, not the probe location (it used to conflate the two and return networks from other countries, #491); `source_country` filters where the traces were measured FROM, if you want that instead. crossing_rate is computed from left_source_country, the general column, not the legacy Canada-only leaves_canada - for CA that is 34,406 crossings rather than 10,093, so rankings and the 10% inclusion threshold differ from before.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"use_case":{"type":"string"},"country":{"type":"string","description":"The prospect network's registered country (ISO2, e.g. \"US\"). Required - no default (#363)."},"min_traffic":{"type":"string"},"min_ixps":{"type":"integer"},"target_ixp":{"type":"integer"},"source_country":{"type":"string","description":"transit use case only: filter by where the traceroutes were measured FROM, as opposed to `country` which is the prospect network's own registered country."},"competitor_ixps":{"type":"string"},"hide_dismissed":{"type":"boolean","default":true},"limit":{"type":"integer","default":50}},"required":["country"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_long_haul_customers":{"post":{"operationId":"find_long_haul_customers","summary":"Networks with long-haul routes (potential transport/backbone customers)","description":"Networks with long-haul routes (potential transport/backbone customers)","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"min_distance_km":{"type":"integer","default":500},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_cloud_connect_prospects":{"post":{"operationId":"get_cloud_connect_prospects","summary":"Networks that are candidates for cloud on-ramp / direct connect, best first. Ranked by prospect_score, then cloud_peering_score, then ASN - the cap is applied AFTER ranking (#492), so a smaller limit returns the best N rather than an arbitrary N.","description":"Networks that are candidates for cloud on-ramp / direct connect, best first. Ranked by prospect_score, then cloud_peering_score, then ASN - the cap is applied AFTER ranking (#492), so a smaller limit returns the best N rather than an arbitrary N.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":50},"country":{"type":"string","description":"ISO 2-letter country code"},"min_score":{"type":"integer","description":"Minimum prospect_score"},"has_cloud_services":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_prospect_tags":{"post":{"operationId":"get_prospect_tags","summary":"Your saved prospect tags (customer/prospect/not_interested) on ASNs. A dismissed lead is stored as the not_interested tag, so this is also the review list for anything dismissed at /prospect.","description":"Your saved prospect tags (customer/prospect/not_interested) on ASNs. A dismissed lead is stored as the not_interested tag, so this is also the review list for anything dismissed at /prospect.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_clients":{"post":{"operationId":"get_clients","summary":"Your CRM clients/accounts and their member ASNs and IXPs - the list behind /prospect/clients. This is where a client_id for add_asn_to_client comes from. Scoped to your own clients plus your sales org's, exactly as the web page is.","description":"Your CRM clients/accounts and their member ASNs and IXPs - the list behind /prospect/clients. This is where a client_id for add_asn_to_client comes from. Scoped to your own clients plus your sales org's, exactly as the web page is.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":100}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_search_templates":{"post":{"operationId":"get_search_templates","summary":"The seller profile, linked assets and target countries used to personalize /templates search recommendations. Requires access level 100, matching the page.","description":"The seller profile, linked assets and target countries used to personalize /templates search recommendations. Requires access level 100, matching the page.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_new_networks":{"post":{"operationId":"get_new_networks","summary":"Recently registered ASNs (new networks), optionally by country","description":"Recently registered ASNs (new networks), optionally by country","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"days":{"type":"integer","default":365},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_growing_networks":{"post":{"operationId":"get_growing_networks","summary":"Fastest-growing networks by facility/IXP expansion","description":"Fastest-growing networks by facility/IXP expansion","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_prefix_growth":{"post":{"operationId":"get_prefix_growth","summary":"Networks with the largest IPv4 prefix growth between the latest two completed RIB snapshots. Returns both snapshot dates.","description":"Networks with the largest IPv4 prefix growth between the latest two completed RIB snapshots. Returns both snapshot dates.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"limit":{"type":"integer","default":30}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_country_benchmark":{"post":{"operationId":"get_country_benchmark","summary":"Per-country network/routing benchmark stats (network counts, IPv6 adoption, IX presence). ipv6_pct is measured-only and reproducible: it is ipv6_networks / ipv6_measured_asns, NOT over total_asns. ipv6_unknown_asns is the population we have never measured and it is excluded from both sides of the percentage rather than counted as \"no IPv6\" (#495). on_ix_pct works the same way and matters more: asn_on_ix is unknown for 48% of networks, so networks_on_ix must NOT be read against total_asns.","description":"Per-country network/routing benchmark stats (network counts, IPv6 adoption, IX presence). ipv6_pct is measured-only and reproducible: it is ipv6_networks / ipv6_measured_asns, NOT over total_asns. ipv6_unknown_asns is the population we have never measured and it is excluded from both sides of the percentage rather than counted as \"no IPv6\" (#495). on_ix_pct works the same way and matters more: asn_on_ix is unknown for 48% of networks, so networks_on_ix must NOT be read against total_asns.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_watchlist":{"post":{"operationId":"get_watchlist","summary":"Everything you are watching. Each row carries subject_type ('asn', 'ixp', 'facility' or 'org') and subject_id; `asn` is filled in only on an ASN row and is null otherwise, which means 'not about a network', not AS0. Add or remove: add_to_watchlist/remove_from_watchlist (ASN), add_ixp_to_watchlist/remove_ixp_from_watchlist (IXP, #350), add_facility_to_watchlist/remove_facility_from_watchlist (facility, #350), add_org_to_watchlist/remove_org_from_watchlist (registry org, #350).","description":"Everything you are watching. Each row carries subject_type ('asn', 'ixp', 'facility' or 'org') and subject_id; `asn` is filled in only on an ASN row and is null otherwise, which means 'not about a network', not AS0. Add or remove: add_to_watchlist/remove_from_watchlist (ASN), add_ixp_to_watchlist/remove_ixp_from_watchlist (IXP, #350), add_facility_to_watchlist/remove_facility_from_watchlist (facility, #350), add_org_to_watchlist/remove_org_from_watchlist (registry org, #350).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_alerts":{"post":{"operationId":"get_alerts","summary":"Your recent alerts/notifications","description":"Your recent alerts/notifications","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"unread_only":{"type":"boolean"},"limit":{"type":"integer","default":30}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_notifications":{"post":{"operationId":"get_notifications","summary":"Your unread in-app notifications (the notification bell), newest first. Distinct from get_alerts, which is watchlist alerts specifically - this is a separate table with its own writer. Dismiss one or all with dismiss_notification (#350).","description":"Your unread in-app notifications (the notification bell), newest first. Distinct from get_alerts, which is watchlist alerts specifically - this is a separate table with its own writer. Dismiss one or all with dismiss_notification (#350).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_saved_searches":{"post":{"operationId":"get_saved_searches","summary":"Your saved searches","description":"Your saved searches","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_auto_scout":{"post":{"operationId":"get_auto_scout","summary":"Your auto-scout (auto-run saved searches) with the latest result counts AND the identifiers that arrived or left since the previous run - so \"3 new\" can name the three (#153)","description":"Your auto-scout (auto-run saved searches) with the latest result counts AND the identifiers that arrived or left since the previous run - so \"3 new\" can name the three (#153)","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_favorites":{"post":{"operationId":"get_favorites","summary":"Your favorite ASNs. Add or remove with add_favorite/remove_favorite (#350).","description":"Your favorite ASNs. Add or remove with add_favorite/remove_favorite (#350).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_dashboard_pins":{"post":{"operationId":"get_dashboard_pins","summary":"Cards pinned to your dashboard (#240) - the same list /dashboard renders, decorated with name, country and a reason to watch each one.","description":"Cards pinned to your dashboard (#240) - the same list /dashboard renders, decorated with name, country and a reason to watch each one.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_dashboard_digest":{"post":{"operationId":"get_dashboard_digest","summary":"Your personal 7-day dashboard digest: facility/IXP count changes on YOUR watched ASNs, watchlist quality alerts, new traceroutes to/from YOUR watched ASNs, and new-match status of YOUR saved searches. Always scoped to the calling principal - never returns another user's watchlist or saved searches. watched_asn_count=0 means you have nothing on your watchlist, which is a real state, not a failure; the ASN/quality/trace sections are only populated when watched_asn_count > 0.","description":"Your personal 7-day dashboard digest: facility/IXP count changes on YOUR watched ASNs, watchlist quality alerts, new traceroutes to/from YOUR watched ASNs, and new-match status of YOUR saved searches. Always scoped to the calling principal - never returns another user's watchlist or saved searches. watched_asn_count=0 means you have nothing on your watchlist, which is a real state, not a failure; the ASN/quality/trace sections are only populated when watched_asn_count > 0.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_my_traceroutes":{"post":{"operationId":"get_my_traceroutes","summary":"Your own submitted traceroute batches, or the traces in one owned batch (pass batch_id). Unlike search_traceroutes, this reads your private submission corpus and always scopes by your user_id. category (only meaningful with batch_id, since a batch listing has no per-trace category) matches like search_traceroutes does (#254): a case-sensitive substring against the per-target label (241 distinct values as of 2026-08-21), not an exact match - an exact \"RIPE Atlas\" only hits the legacy label used before the collector switched to per-target labels in April 2026.","description":"Your own submitted traceroute batches, or the traces in one owned batch (pass batch_id). Unlike search_traceroutes, this reads your private submission corpus and always scopes by your user_id. category (only meaningful with batch_id, since a batch listing has no per-trace category) matches like search_traceroutes does (#254): a case-sensitive substring against the per-target label (241 distinct values as of 2026-08-21), not an exact match - an exact \"RIPE Atlas\" only hits the legacy label used before the collector switched to per-target labels in April 2026.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"batch_id":{"type":"integer"},"archived":{"type":"boolean","default":false},"status":{"type":"string"},"category":{"type":"string","description":"Substring match against the per-target label. Only applies when batch_id is given."},"page":{"type":"integer","default":1},"limit":{"type":"integer","default":25}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/archive_trace_batch":{"post":{"operationId":"archive_trace_batch","summary":"Archive one of your submitted traceroute batches, matching the Archive button on /my/traceroutes. Archived batches are hidden from get_my_traceroutes unless archived:true is passed.","description":"Archive one of your submitted traceroute batches, matching the Archive button on /my/traceroutes. Archived batches are hidden from get_my_traceroutes unless archived:true is passed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"batch_id":{"type":"integer"}},"required":["batch_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/unarchive_trace_batch":{"post":{"operationId":"unarchive_trace_batch","summary":"Restore an archived traceroute batch of yours.","description":"Restore an archived traceroute batch of yours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"batch_id":{"type":"integer"}},"required":["batch_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/exclude_trace_batch_from_transit":{"post":{"operationId":"exclude_trace_batch_from_transit","summary":"Include or exclude one of your traceroute batches from the Transit Analyser (#348), matching /my/traceroutes' per-batch toggle. exclude:true removes it from that wizard's candidate pool without deleting or archiving the batch.","description":"Include or exclude one of your traceroute batches from the Transit Analyser (#348), matching /my/traceroutes' per-batch toggle. exclude:true removes it from that wizard's candidate pool without deleting or archiving the batch.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"batch_id":{"type":"integer"},"exclude":{"type":"boolean","default":true}},"required":["batch_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_to_watchlist":{"post":{"operationId":"add_to_watchlist","summary":"Add an ASN to your watchlist. Idempotent: calling this again for an ASN you already watch UPDATES its notes/share_with_org rather than duplicating the row - that is also how to change them later.","description":"Add an ASN to your watchlist. Idempotent: calling this again for an ASN you already watch UPDATES its notes/share_with_org rather than duplicating the row - that is also how to change them later.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"notes":{"type":"string"},"share_with_org":{"type":"boolean"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_from_watchlist":{"post":{"operationId":"remove_from_watchlist","summary":"Remove an ASN from your watchlist","description":"Remove an ASN from your watchlist","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_ixp_to_watchlist":{"post":{"operationId":"add_ixp_to_watchlist","summary":"Follow an IXP - alerts on membership joins/leaves and port-speed changes (#350). Same idempotent-update behaviour as add_to_watchlist: call again to change notes/share_with_org.","description":"Follow an IXP - alerts on membership joins/leaves and port-speed changes (#350). Same idempotent-update behaviour as add_to_watchlist: call again to change notes/share_with_org.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ix_id":{"type":"integer","description":"PeeringDB exchange id, from get_ixp_info"},"notes":{"type":"string"},"share_with_org":{"type":"boolean"}},"required":["ix_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_ixp_from_watchlist":{"post":{"operationId":"remove_ixp_from_watchlist","summary":"Unfollow an IXP.","description":"Unfollow an IXP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ix_id":{"type":"integer"}},"required":["ix_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_facility_to_watchlist":{"post":{"operationId":"add_facility_to_watchlist","summary":"Watch a facility (data center) - the same Watch button as a facility page (#350). Alerts on membership changes at that building. Idempotent: calling again updates notes/share_with_org.","description":"Watch a facility (data center) - the same Watch button as a facility page (#350). Alerts on membership changes at that building. Idempotent: calling again updates notes/share_with_org.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"facility_id":{"type":"integer","description":"PeeringDB facility id, from get_facility_info"},"notes":{"type":"string"},"share_with_org":{"type":"boolean"}},"required":["facility_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_facility_from_watchlist":{"post":{"operationId":"remove_facility_from_watchlist","summary":"Unwatch a facility.","description":"Unwatch a facility.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"facility_id":{"type":"integer"}},"required":["facility_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_org_to_watchlist":{"post":{"operationId":"add_org_to_watchlist","summary":"Watch a registry organisation (RIR org) - the same Watch button as an org page (#350). org_key is the opaque_id from lookup_rir_org. Idempotent: calling again updates notes/share_with_org.","description":"Watch a registry organisation (RIR org) - the same Watch button as an org page (#350). org_key is the opaque_id from lookup_rir_org. Idempotent: calling again updates notes/share_with_org.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"registry":{"type":"string","description":"arin | ripencc | apnic | lacnic | afrinic"},"org_key":{"type":"string","description":"The registry's opaque org id, as returned by lookup_rir_org"},"notes":{"type":"string"},"share_with_org":{"type":"boolean"}},"required":["registry","org_key"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_org_from_watchlist":{"post":{"operationId":"remove_org_from_watchlist","summary":"Unwatch a registry organisation.","description":"Unwatch a registry organisation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"registry":{"type":"string"},"org_key":{"type":"string"}},"required":["registry","org_key"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/pin_to_dashboard":{"post":{"operationId":"pin_to_dashboard","summary":"Pin an item to your dashboard (#240, #350). type: asn | ixp | facility | org.","description":"Pin an item to your dashboard (#240, #350). type: asn | ixp | facility | org.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"asn | ixp | facility | org"},"id":{"type":"integer","description":"The subject id: ASN number, or the PeeringDB id for ixp/facility, or asn.org_id for org"}},"required":["type","id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/unpin_from_dashboard":{"post":{"operationId":"unpin_from_dashboard","summary":"Remove a pinned item from your dashboard.","description":"Remove a pinned item from your dashboard.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string"},"id":{"type":"integer"}},"required":["type","id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_favorite":{"post":{"operationId":"add_favorite","summary":"Favorite an ASN - the same star button as the ASN page (#350).","description":"Favorite an ASN - the same star button as the ASN page (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_favorite":{"post":{"operationId":"remove_favorite","summary":"Un-favorite an ASN (#350).","description":"Un-favorite an ASN (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_trace_favorites":{"post":{"operationId":"get_trace_favorites","summary":"Your favorited traceroutes (the star on a trace-detail page), newest favorite first. Add or remove with add_trace_favorite/remove_trace_favorite (#350). This is a different favorites list from get_favorites, which is ASNs.","description":"Your favorited traceroutes (the star on a trace-detail page), newest favorite first. Add or remove with add_trace_favorite/remove_trace_favorite (#350). This is a different favorites list from get_favorites, which is ASNs.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_trace_favorite":{"post":{"operationId":"add_trace_favorite","summary":"Favorite a traceroute - the same star as the trace-detail page (#350). trace_id is the id from search_traceroutes/get_my_traceroutes, not a batch_id.","description":"Favorite a traceroute - the same star as the trace-detail page (#350). trace_id is the id from search_traceroutes/get_my_traceroutes, not a batch_id.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"trace_id":{"type":"integer"}},"required":["trace_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_trace_favorite":{"post":{"operationId":"remove_trace_favorite","summary":"Un-favorite a traceroute (#350).","description":"Un-favorite a traceroute (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"trace_id":{"type":"integer"}},"required":["trace_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/report_data_issue":{"post":{"operationId":"report_data_issue","summary":"Report incorrect or missing data on an ASN, IXP, facility or org, the same form as the page's \"Report incorrect data\" link (#350). subject_type: asn | ixp | facility | org. subject_id is the ASN number, PeeringDB id (ixp/facility), or org registry id/slug.","description":"Report incorrect or missing data on an ASN, IXP, facility or org, the same form as the page's \"Report incorrect data\" link (#350). subject_type: asn | ixp | facility | org. subject_id is the ASN number, PeeringDB id (ixp/facility), or org registry id/slug.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subject_type":{"type":"string","description":"asn | ixp | facility | org"},"subject_id":{"type":"string"},"description":{"type":"string"}},"required":["subject_type","subject_id","description"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/tag_prospect":{"post":{"operationId":"tag_prospect","summary":"Tag an ASN as a sales prospect. tag: not_interested | prospected | customer | high_priority. Dismissing a lead at /prospect (the \"Not interested\" action) is the same write as tag_prospect(tag: 'not_interested') - there is no separate dismiss tool (#350).","description":"Tag an ASN as a sales prospect. tag: not_interested | prospected | customer | high_priority. Dismissing a lead at /prospect (the \"Not interested\" action) is the same write as tag_prospect(tag: 'not_interested') - there is no separate dismiss tool (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"tag":{"type":"string"},"notes":{"type":"string"}},"required":["asn","tag"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/untag_prospect":{"post":{"operationId":"untag_prospect","summary":"Remove your prospect tag from an ASN, whatever it is. Scoped to your own row only. To specifically undo a not_interested dismissal - including one an org-mate applied - use undismiss_prospect instead (#350).","description":"Remove your prospect tag from an ASN, whatever it is. Scoped to your own row only. To specifically undo a not_interested dismissal - including one an org-mate applied - use undismiss_prospect instead (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/undismiss_prospect":{"post":{"operationId":"undismiss_prospect","summary":"Undo a \"not interested\" dismissal on an ASN - yours or any dismissal by your sales org - without touching a different tag (customer/prospected/high_priority) that might also be set. Matches POST /prospect/undismiss exactly; NOT the same as untag_prospect, which only removes your own tag and is not restricted to the not_interested tag (#350).","description":"Undo a \"not interested\" dismissal on an ASN - yours or any dismissal by your sales org - without touching a different tag (customer/prospected/high_priority) that might also be set. Matches POST /prospect/undismiss exactly; NOT the same as untag_prospect, which only removes your own tag and is not restricted to the not_interested tag (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/save_search":{"post":{"operationId":"save_search","summary":"Save a search by name and its URL (e.g. \"/asns?country=CA&cdn=akamai\")","description":"Save a search by name and its URL (e.g. \"/asns?country=CA&cdn=akamai\")","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"search_url":{"type":"string"}},"required":["name","search_url"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_saved_search":{"post":{"operationId":"delete_saved_search","summary":"Delete one of your saved searches by id (id from get_saved_searches). #350.","description":"Delete one of your saved searches by id (id from get_saved_searches). #350.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/create_auto_scout":{"post":{"operationId":"create_auto_scout","summary":"Create an auto-scout that re-runs a saved search on a schedule and alerts when the result count changes","description":"Create an auto-scout that re-runs a saved search on a schedule and alerts when the result count changes","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"search_url":{"type":"string"},"frequency":{"type":"string","description":"daily | weekly | monthly"}},"required":["name","search_url"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/set_auto_scout_frequency":{"post":{"operationId":"set_auto_scout_frequency","summary":"Change an auto-scout run frequency (daily | weekly | monthly) by its id","description":"Change an auto-scout run frequency (daily | weekly | monthly) by its id","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"frequency":{"type":"string"}},"required":["id","frequency"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_auto_scout":{"post":{"operationId":"delete_auto_scout","summary":"Delete one of your auto-scouts by id. Requires access level 100 and is scoped to your user_id.","description":"Delete one of your auto-scouts by id. Requires access level 100 and is scoped to your user_id.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/create_client":{"post":{"operationId":"create_client","summary":"Create a CRM client/account in your sales org. tag: customer | prospect | not_interested | partner | high_priority","description":"Create a CRM client/account in your sales org. tag: customer | prospect | not_interested | partner | high_priority","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"tag":{"type":"string"},"notes":{"type":"string"}},"required":["name"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_asn_to_client":{"post":{"operationId":"add_asn_to_client","summary":"Add an ASN to one of your CRM clients (client_id from get_prospect_tags or create_client)","description":"Add an ASN to one of your CRM clients (client_id from get_prospect_tags or create_client)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"integer"},"asn":{"type":"integer"}},"required":["client_id","asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_asn_from_client":{"post":{"operationId":"remove_asn_from_client","summary":"Remove an ASN from one of your CRM clients, without deleting the client. #350.","description":"Remove an ASN from one of your CRM clients, without deleting the client. #350.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"integer"},"asn":{"type":"integer"}},"required":["client_id","asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_client":{"post":{"operationId":"delete_client","summary":"Delete one of your CRM clients/accounts and all its members. Scoped to clients you own or your sales org shares. #350.","description":"Delete one of your CRM clients/accounts and all its members. Scoped to clients you own or your sales org shares. #350.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"integer"}},"required":["client_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/mark_alerts_read":{"post":{"operationId":"mark_alerts_read","summary":"Mark your alerts read - all of them, or one by alert_id","description":"Mark your alerts read - all of them, or one by alert_id","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"alert_id":{"type":"integer"},"all":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/archive_alerts":{"post":{"operationId":"archive_alerts","summary":"Archive and mark read all alerts belonging to you and your sales org, matching /alerts/archive-all.","description":"Archive and mark read all alerts belonging to you and your sales org, matching /alerts/archive-all.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/dismiss_notification":{"post":{"operationId":"dismiss_notification","summary":"Dismiss one of your in-app notifications by id (from get_notifications), or all of them if id is omitted.","description":"Dismiss one of your in-app notifications by id (from get_notifications), or all of them if id is omitted.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/list_watch_rule_types":{"post":{"operationId":"list_watch_rule_types","summary":"The alert-rule vocabulary: every subject/event combination a rule can be built on, the optional conditions each supports, and - just as importantly - the combinations that are deliberately WITHHELD because nothing in the system emits the event, each with the silent table and the reason. Call this before create_watch_rule. A rule type that is not listed as offered cannot be created, and asking for one returns the reason rather than an empty result.","description":"The alert-rule vocabulary: every subject/event combination a rule can be built on, the optional conditions each supports, and - just as importantly - the combinations that are deliberately WITHHELD because nothing in the system emits the event, each with the silent table and the reason. Call this before create_watch_rule. A rule type that is not listed as offered cannot be created, and asking for one returns the reason rather than an empty result.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/create_watch_rule":{"post":{"operationId":"create_watch_rule","summary":"Create an alert rule. rule_key comes from list_watch_rule_types; subject_value is the ASN, the PeeringDB exchange or facility id, or a registry key like \"arin:C12345678\"; predicate_key and predicate_value are optional and must be one of the conditions that rule type supports; frequency is daily | weekly | monthly. Evaluated by the nightly job, so the first result arrives with the next run - a new rule subscribes you to its subject's FUTURE and never replays history. Requires the Search Scout feature.","description":"Create an alert rule. rule_key comes from list_watch_rule_types; subject_value is the ASN, the PeeringDB exchange or facility id, or a registry key like \"arin:C12345678\"; predicate_key and predicate_value are optional and must be one of the conditions that rule type supports; frequency is daily | weekly | monthly. Evaluated by the nightly job, so the first result arrives with the next run - a new rule subscribes you to its subject's FUTURE and never replays history. Requires the Search Scout feature.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"rule_key":{"type":"string"},"subject_value":{"type":"string"},"predicate_key":{"type":"string"},"predicate_value":{"type":"string"},"frequency":{"type":"string","description":"daily | weekly | monthly"},"name":{"type":"string"},"share_with_org":{"type":"boolean"}},"required":["rule_key","subject_value"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/preview_watch_rule":{"post":{"operationId":"preview_watch_rule","summary":"Dry-run an alert rule before creating it: what would it have matched over the last N days? Same fields as create_watch_rule (rule_key, subject_value, predicate_key, predicate_value), no rule is saved. A refusal (evaluable:false) means the preview could not be run - it does NOT mean the rule matches nothing (#350).","description":"Dry-run an alert rule before creating it: what would it have matched over the last N days? Same fields as create_watch_rule (rule_key, subject_value, predicate_key, predicate_value), no rule is saved. A refusal (evaluable:false) means the preview could not be run - it does NOT mean the rule matches nothing (#350).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"rule_key":{"type":"string"},"subject_value":{"type":"string"},"predicate_key":{"type":"string"},"predicate_value":{"type":"string"},"days":{"type":"integer","default":365}},"required":["rule_key","subject_value"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/list_watch_rules":{"post":{"operationId":"list_watch_rules","summary":"Your alert rules and your org-shared ones, each with the outcome of its last evaluation. A status of \"unevaluable\" means the rule could NOT be run - it is not zero matches. A status of \"never_observed\" means the rule's event types have never produced a row anywhere, which is different again.","description":"Your alert rules and your org-shared ones, each with the outcome of its last evaluation. A status of \"unevaluable\" means the rule could NOT be run - it is not zero matches. A status of \"never_observed\" means the rule's event types have never produced a row anywhere, which is different again.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_watch_rule":{"post":{"operationId":"delete_watch_rule","summary":"Delete one of your alert rules by id, with its evaluation history. Scoped to rules you own or your org shares; a rule you cannot see is reported as not found.","description":"Delete one of your alert rules by id, with its evaluation history. Scoped to rules you own or your org shares; a rule you cannot see is reported as not found.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/set_watch_rule_active":{"post":{"operationId":"set_watch_rule_active","summary":"Enable or disable one of your alert rules by id, WITHOUT deleting its evaluation history the way delete_watch_rule + create_watch_rule would. Requires the Search Scout feature. #350.","description":"Enable or disable one of your alert rules by id, WITHOUT deleting its evaluation history the way delete_watch_rule + create_watch_rule would. Requires the Search Scout feature. #350.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"active":{"type":"boolean"}},"required":["id","active"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/update_watch_rule":{"post":{"operationId":"update_watch_rule","summary":"Change an existing alert rule's settings by id: name, frequency, predicate_key/predicate_value, share_with_org, active. What the rule watches (its rule_key/subject) cannot be changed this way - delete_watch_rule + create_watch_rule for that. Fields you omit are left unchanged. Requires the Search Scout feature.","description":"Change an existing alert rule's settings by id: name, frequency, predicate_key/predicate_value, share_with_org, active. What the rule watches (its rule_key/subject) cannot be changed this way - delete_watch_rule + create_watch_rule for that. Fields you omit are left unchanged. Requires the Search Scout feature.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"frequency":{"type":"string","description":"daily | weekly | monthly"},"predicate_key":{"type":"string"},"predicate_value":{"type":"string"},"share_with_org":{"type":"boolean"},"active":{"type":"boolean"}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/list_alert_destinations":{"post":{"operationId":"list_alert_destinations","summary":"Your alert delivery destinations. Never returns the webhook signing secret or a rotation's previous_secret - both are shown once, at creation/rotation time (create_alert_destination / update_alert_destination's rotate_secret), and never again after that. Each entry's enabled/verified/unsubscribed state and its type-specific target (webhookUrl for a webhook; emailAddress for email, though email cannot be created yet - see create_alert_destination) are included.","description":"Your alert delivery destinations. Never returns the webhook signing secret or a rotation's previous_secret - both are shown once, at creation/rotation time (create_alert_destination / update_alert_destination's rotate_secret), and never again after that. Each entry's enabled/verified/unsubscribed state and its type-specific target (webhookUrl for a webhook; emailAddress for email, though email cannot be created yet - see create_alert_destination) are included.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/create_alert_destination":{"post":{"operationId":"create_alert_destination","summary":"Register a new webhook alert destination. Created DISABLED (enabled:false) - never armed by the act of saving it; enable it explicitly via update_alert_destination once you have confirmed it with test_alert_destination. url is SSRF-validated at save time (HTTPS only; loopback/RFC1918/link-local/CGNAT/multicast/reserved/cloud-metadata ranges rejected for IPv4 and IPv6; no embedded credentials; port 443 only) and re-validated fresh on every future delivery attempt, since a DNS answer can change after the row is saved. Returns a one-time HMAC signing secret (webhook_secret) for verifying the BGPScout-Signature header on what you receive - shown ONLY on this call; if lost, rotate it via update_alert_destination's rotate_secret. type:\"email\" is refused: this deployment has no mail provider configured, so an email destination could never receive anything - only \"webhook\" is accepted.","description":"Register a new webhook alert destination. Created DISABLED (enabled:false) - never armed by the act of saving it; enable it explicitly via update_alert_destination once you have confirmed it with test_alert_destination. url is SSRF-validated at save time (HTTPS only; loopback/RFC1918/link-local/CGNAT/multicast/reserved/cloud-metadata ranges rejected for IPv4 and IPv6; no embedded credentials; port 443 only) and re-validated fresh on every future delivery attempt, since a DNS answer can change after the row is saved. Returns a one-time HMAC signing secret (webhook_secret) for verifying the BGPScout-Signature header on what you receive - shown ONLY on this call; if lost, rotate it via update_alert_destination's rotate_secret. type:\"email\" is refused: this deployment has no mail provider configured, so an email destination could never receive anything - only \"webhook\" is accepted.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"Only \"webhook\" is currently supported."},"label":{"type":"string","description":"A name for this destination, e.g. \"NOC critical alerts\". Max 120 characters."},"url":{"type":"string","description":"The HTTPS endpoint to POST signed alert events to."}},"required":["type","url"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/update_alert_destination":{"post":{"operationId":"update_alert_destination","summary":"Change one of your alert destinations by id. Fields you omit are left unchanged. url (webhook only) is SSRF-validated again at update time, and if it actually changes, the destination is automatically set back to disabled - the same \"not armed until confirmed\" rule create_alert_destination applies, since the delivery target itself has changed. rotate_secret:true issues a brand-new HMAC signing secret (returned once, as webhook_secret) while the previous one stays valid for 24h so an in-flight receiver update is not broken mid-rotation; it does not touch label/url/enabled and cannot be combined with changing them in a way that skips the disable - pass it alone if that is all you want to do. A destination_id you do not own is reported exactly like one that does not exist.","description":"Change one of your alert destinations by id. Fields you omit are left unchanged. url (webhook only) is SSRF-validated again at update time, and if it actually changes, the destination is automatically set back to disabled - the same \"not armed until confirmed\" rule create_alert_destination applies, since the delivery target itself has changed. rotate_secret:true issues a brand-new HMAC signing secret (returned once, as webhook_secret) while the previous one stays valid for 24h so an in-flight receiver update is not broken mid-rotation; it does not touch label/url/enabled and cannot be combined with changing them in a way that skips the disable - pass it alone if that is all you want to do. A destination_id you do not own is reported exactly like one that does not exist.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"destination_id":{"type":"integer"},"label":{"type":"string"},"url":{"type":"string"},"enabled":{"type":"boolean"},"rotate_secret":{"type":"boolean"}},"required":["destination_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_alert_destination":{"post":{"operationId":"delete_alert_destination","summary":"Permanently delete one of your alert destinations by id: its signing secret, its watch_rule opt-ins (watch_rule_delivery), and - cancelling rather than erasing, so delivery history stays auditable - any still-pending queued deliveries for it. Already-delivered/dead delivery history (alert_delivery_queue / alert_delivery_attempt rows) is retained. A destination_id you do not own is reported exactly like one that does not exist.","description":"Permanently delete one of your alert destinations by id: its signing secret, its watch_rule opt-ins (watch_rule_delivery), and - cancelling rather than erasing, so delivery history stays auditable - any still-pending queued deliveries for it. Already-delivered/dead delivery history (alert_delivery_queue / alert_delivery_attempt rows) is retained. A destination_id you do not own is reported exactly like one that does not exist.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"destination_id":{"type":"integer"}},"required":["destination_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/test_alert_destination":{"post":{"operationId":"test_alert_destination","summary":"Send a clearly-labelled synthetic test event (test:true in the envelope) to one of your webhook destinations and report the outcome. Does NOT create a real watchlist_alert and never touches the delivery queue, so it cannot be mistaken for a real alert on either end. Only ever sends to the destination's own already-validated, already-stored URL - never a caller-supplied one - so this cannot be used to probe an arbitrary address, and SSRF validation still re-runs fresh on every call regardless. Rate-limited per caller (5 per rolling 5 minutes), since this is the one tool in this group that reaches the network. A destination_id you do not own is reported exactly like one that does not exist.","description":"Send a clearly-labelled synthetic test event (test:true in the envelope) to one of your webhook destinations and report the outcome. Does NOT create a real watchlist_alert and never touches the delivery queue, so it cannot be mistaken for a real alert on either end. Only ever sends to the destination's own already-validated, already-stored URL - never a caller-supplied one - so this cannot be used to probe an arbitrary address, and SSRF validation still re-runs fresh on every call regardless. Rate-limited per caller (5 per rolling 5 minutes), since this is the one tool in this group that reaches the network. A destination_id you do not own is reported exactly like one that does not exist.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"destination_id":{"type":"integer"},"summary":{"type":"string","description":"Optional custom summary text for the synthetic test event."}},"required":["destination_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_alert_deliveries":{"post":{"operationId":"get_alert_deliveries","summary":"Bounded delivery history for your alert destinations: state (pending | sent | dead | cancelled), attempt count, and the most recent attempt's outcome class - \"delivered\" (2xx), \"endpoint returned 500\" (retryable_http/permanent_http, with the real HTTP status in response_code), \"endpoint unreachable\" (timeout/connection_error/ssrf_rejected, response_code null), or \"never attempted\" (no attempt row exists yet at all - a distinct, absence-based fourth state, not a zero or a guess). Never returns request bodies, response bodies or secrets. Filter by destination_id and/or state; bounded by limit (default 50, max 200).","description":"Bounded delivery history for your alert destinations: state (pending | sent | dead | cancelled), attempt count, and the most recent attempt's outcome class - \"delivered\" (2xx), \"endpoint returned 500\" (retryable_http/permanent_http, with the real HTTP status in response_code), \"endpoint unreachable\" (timeout/connection_error/ssrf_rejected, response_code null), or \"never attempted\" (no attempt row exists yet at all - a distinct, absence-based fourth state, not a zero or a guess). Never returns request bodies, response bodies or secrets. Filter by destination_id and/or state; bounded by limit (default 50, max 200).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"destination_id":{"type":"integer"},"state":{"type":"string","description":"pending | sent | dead | cancelled"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_prefixes":{"post":{"operationId":"get_asn_prefixes","summary":"Paginate IPv4/IPv6 prefixes observed with an ASN as origin. Snapshot counts are true totals, not the size of this page. Missing or inconsistent coverage is explicit; private/reserved ASNs are labelled as anomalous collector observations, not registered networks.","description":"Paginate IPv4/IPv6 prefixes observed with an ASN as origin. Snapshot counts are true totals, not the size of this page. Missing or inconsistent coverage is explicit; private/reserved ASNs are labelled as anomalous collector observations, not registered networks.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"family":{"type":"string","description":"4|ipv4|6|ipv6; omit for both"},"page":{"type":"integer","default":1},"per_page":{"type":"integer","default":50,"maximum":200},"sort":{"type":"string","enum":["prefix","family","hits"]},"direction":{"type":"string","enum":["asc","desc"]}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_ipv6_reachability":{"post":{"operationId":"get_asn_ipv6_reachability","summary":"Evidence that a network's IPv6 actually WORKS, not just that it announces v6 space (#54). Returns the strongest level observed and every supporting observation with its date and vantage point. Levels, weakest to strongest: announced (announces v6 prefixes - NOT reachability evidence) < ixp_port (v6 configured on an IXP port, PeeringDB self-report) < aaaa (a name of theirs resolves AAAA) < path (a router in their own v6 space answered a traceroute) < icmp6 (their own space answered our ping) < tcp6 (a TCP handshake completed into their own space). An unconfirmed result means we have not observed it working - most ASNs have never been probed - and NEVER means the network has no IPv6.","description":"Evidence that a network's IPv6 actually WORKS, not just that it announces v6 space (#54). Returns the strongest level observed and every supporting observation with its date and vantage point. Levels, weakest to strongest: announced (announces v6 prefixes - NOT reachability evidence) < ixp_port (v6 configured on an IXP port, PeeringDB self-report) < aaaa (a name of theirs resolves AAAA) < path (a router in their own v6 space answered a traceroute) < icmp6 (their own space answered our ping) < tcp6 (a TCP handshake completed into their own space). An unconfirmed result means we have not observed it working - most ASNs have never been probed - and NEVER means the network has no IPv6.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/lookup_prefix":{"post":{"operationId":"lookup_prefix","summary":"For an IPv4 CIDR (e.g. 8.8.8.0/24), the ASN that announces the covering prefix. `bogon` is set (and `announced_by` is null) for IANA special-use/reserved ranges - these resolve immediately rather than scanning the routing table for an announcement that cannot exist (#480).","description":"For an IPv4 CIDR (e.g. 8.8.8.0/24), the ASN that announces the covering prefix. `bogon` is set (and `announced_by` is null) for IANA special-use/reserved ranges - these resolve immediately rather than scanning the routing table for an announcement that cannot exist (#480).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cidr":{"type":"string"}},"required":["cidr"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_bogons":{"post":{"operationId":"get_asn_bogons","summary":"Bogon prefixes an ASN announces - IANA special-use space (RFC 1918, loopback, documentation, multicast, …) or space no RIR has delegated. Omit asn to list the ASNs announcing the most bogons. severity: high (special-use) | medium (undelegated) | low (registry holds it reserved; advisory only)","description":"Bogon prefixes an ASN announces - IANA special-use space (RFC 1918, loopback, documentation, multicast, …) or space no RIR has delegated. Omit asn to list the ASNs announcing the most bogons. severity: high (special-use) | medium (undelegated) | low (registry holds it reserved; advisory only)","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"type":{"type":"string","description":"special_use | unallocated | reserved | confirmed (special_use + unallocated)"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_rpki_status":{"post":{"operationId":"get_asn_rpki_status","summary":"RPKI route-origin validation summary for one ASN, from the bulk validation of announced_prefix against rpki_roa (#551 slice 1). Four states: valid, invalid, notfound (the majority state - no ROA covers the prefix, NOT a failure), unavailable (could not be evaluated). Never presents notfound as invalid. Not incident detection - no baseline, no history, just the current verdict per prefix. See get_rpki_validation_history for the change-over-time series built on top of this.","description":"RPKI route-origin validation summary for one ASN, from the bulk validation of announced_prefix against rpki_roa (#551 slice 1). Four states: valid, invalid, notfound (the majority state - no ROA covers the prefix, NOT a failure), unavailable (could not be evaluated). Never presents notfound as invalid. Not incident detection - no baseline, no history, just the current verdict per prefix. See get_rpki_validation_history for the change-over-time series built on top of this.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"include_invalid_prefixes":{"type":"boolean","default":false,"description":"include the list of this ASN's invalid prefixes with their contradicting ROA info"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_rpki_validation_history":{"post":{"operationId":"get_rpki_validation_history","summary":"RPKI validation-state TRANSITIONS - when a prefix's verdict (valid/invalid/notfound/unavailable) or invalid REASON (wrong_asn/too_long/both) genuinely differed between two consecutive nightly runs of the bulk RPKI validator. This is a change-series over ROA/configuration drift, and it is explicitly NOT incident detection: the validator samples once per ~24h, so `detected_at` on every transition is when the DIFFERENCE was first noticed, never when the underlying ROA or announcement actually changed - the real change could have happened, and fully resolved, at any point inside that window. An event that both starts and ends between two nightly samples produces ZERO rows here, structurally, forever; that is the sampling rate, not a bug, and an empty `transitions` list must never be read as 'nothing happened' without also checking `latest_comparison_run` (see below). Never call a transition a hijack or an incident: per rpki_invalid_breakdown.py's own finding on the corpus, an RPKI-invalid announcement is usually a stale or misconfigured ROA, not an attack, and a transition INTO invalid inherits that same caveat. `latest_comparison_run.has_baseline` is a FIRST-CLASS FACT, distinct from an empty result (#67): has_baseline=0 means no prior observation existed for ANY prefix when that run executed (a from-cold-start seed, e.g. the very first run) - genuinely different from has_baseline=1 with transitions_found=0, which means a real comparison ran and found nothing to report. As of 2026-08-26 exactly two comparison runs exist (run 1: has_baseline=0, the cold start; run 2: has_baseline=1, transitions_found=0, a real quiet comparison) and BOTH will legitimately return an empty `transitions` array for any filter - that is correct given only one underlying nightly rpki_validate_announced.py execution has happened, not evidence this tool is broken. Keyed on (asn, cidr), never announced_prefix_id - that id is reassigned nightly by a rebuild-and-swap and is not stable across runs (verified: 1,392,531 distinct (asn,cidr) pairs for 1,392,531 announced_prefix rows). Pass asn alone for every transition touching that ASN's prefixes; add cidr for one exact prefix (and current_state is then also returned, the LATEST known baseline verdict for that pair); pass neither for the global recent-transitions feed. state/reason filter on the NEW (destination) state/reason of the transition.","description":"RPKI validation-state TRANSITIONS - when a prefix's verdict (valid/invalid/notfound/unavailable) or invalid REASON (wrong_asn/too_long/both) genuinely differed between two consecutive nightly runs of the bulk RPKI validator. This is a change-series over ROA/configuration drift, and it is explicitly NOT incident detection: the validator samples once per ~24h, so `detected_at` on every transition is when the DIFFERENCE was first noticed, never when the underlying ROA or announcement actually changed - the real change could have happened, and fully resolved, at any point inside that window. An event that both starts and ends between two nightly samples produces ZERO rows here, structurally, forever; that is the sampling rate, not a bug, and an empty `transitions` list must never be read as 'nothing happened' without also checking `latest_comparison_run` (see below). Never call a transition a hijack or an incident: per rpki_invalid_breakdown.py's own finding on the corpus, an RPKI-invalid announcement is usually a stale or misconfigured ROA, not an attack, and a transition INTO invalid inherits that same caveat. `latest_comparison_run.has_baseline` is a FIRST-CLASS FACT, distinct from an empty result (#67): has_baseline=0 means no prior observation existed for ANY prefix when that run executed (a from-cold-start seed, e.g. the very first run) - genuinely different from has_baseline=1 with transitions_found=0, which means a real comparison ran and found nothing to report. As of 2026-08-26 exactly two comparison runs exist (run 1: has_baseline=0, the cold start; run 2: has_baseline=1, transitions_found=0, a real quiet comparison) and BOTH will legitimately return an empty `transitions` array for any filter - that is correct given only one underlying nightly rpki_validate_announced.py execution has happened, not evidence this tool is broken. Keyed on (asn, cidr), never announced_prefix_id - that id is reassigned nightly by a rebuild-and-swap and is not stable across runs (verified: 1,392,531 distinct (asn,cidr) pairs for 1,392,531 announced_prefix rows). Pass asn alone for every transition touching that ASN's prefixes; add cidr for one exact prefix (and current_state is then also returned, the LATEST known baseline verdict for that pair); pass neither for the global recent-transitions feed. state/reason filter on the NEW (destination) state/reason of the transition.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"Origin ASN. Optional; omit for the global feed."},"cidr":{"type":"string","description":"Exact prefix, e.g. \"203.0.113.0/24\". Requires asn. When given, also returns current_state (the latest baseline verdict for this exact pair)."},"state":{"type":"string","enum":["valid","invalid","notfound","unavailable"],"description":"Filter to transitions whose NEW state matches (e.g. state=invalid finds prefixes that just became invalid)."},"reason":{"type":"string","enum":["wrong_asn","too_long","both"],"description":"Filter to transitions whose NEW invalid reason matches. Only meaningful alongside state=invalid."},"limit":{"type":"integer","default":50,"description":"Max transitions per page. Capped at 500."},"cursor":{"type":"integer","description":"Pass the previous response's next_cursor to page further back in time."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_rpki_comparison_runs":{"post":{"operationId":"get_rpki_comparison_runs","summary":"Audit trail of backend/rpki_validation_history.py executions - one row per run, always exactly one regardless of how many prefixes exist, so this answers 'has the RPKI change-detection job actually been running' independent of whether it found anything. Each row's `has_baseline` is the #67 distinction get_rpki_validation_history depends on: 0 means no prior observation existed for ANY prefix when THAT run executed (first-run/cold-start), 1 means a real prior baseline existed and `transitions_found` is a genuine comparison result (0 there means 'compared, found nothing', not 'did not compare'). `prefixes_new`/`prefixes_vanished`/`prefixes_compared` describe that run's (asn,cidr) coverage against the prior baseline. This is diagnostic/meta - most callers asking about a specific prefix or ASN want get_rpki_validation_history instead.","description":"Audit trail of backend/rpki_validation_history.py executions - one row per run, always exactly one regardless of how many prefixes exist, so this answers 'has the RPKI change-detection job actually been running' independent of whether it found anything. Each row's `has_baseline` is the #67 distinction get_rpki_validation_history depends on: 0 means no prior observation existed for ANY prefix when THAT run executed (first-run/cold-start), 1 means a real prior baseline existed and `transitions_found` is a genuine comparison result (0 there means 'compared, found nothing', not 'did not compare'). `prefixes_new`/`prefixes_vanished`/`prefixes_compared` describe that run's (asn,cidr) coverage against the prior baseline. This is diagnostic/meta - most callers asking about a specific prefix or ASN want get_rpki_validation_history instead.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"type":"integer","default":20,"description":"Max runs to return, most recent first. Capped at 200."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_irr_objects":{"post":{"operationId":"get_irr_objects","summary":"Look up IRR/RPSL objects (route, route6, aut-num, as-set) directly from the public IRR registries - RADB, RIPE, APNIC, ARIN, LEVEL3, NTTCOM - live, on demand (short-TTL cache; not a bulk mirror). Give ONE of: `prefix` (route/route6 exact-match), `asn` (aut-num plus an inverse origin lookup across route/route6), or `object_name` (an as-set/route-set/aut-num/maintainer primary key, e.g. \"AS-EXAMPLE\"). EVERY requested source is queried INDEPENDENTLY and every object it returns is kept separate - two sources both holding the same name are NEVER merged, because IRR data is genuinely contradictory across registries (measured live: RADB and RIPE hold two different \"AS-GOOGLE\" as-set objects with different members). `source_status` reports what happened for EACH requested source on EACH sub-query - `ok` (answered with data), `not_found` (answered \"no entries\", a real positive answer), `error` (answered but the reply was rejected), or `unreachable` (did not answer at all, e.g. connect timeout) - these are different facts and are never collapsed into one (#67); do not read a `not_found` on one source as evidence the object doesn't exist anywhere, and do not read a source missing from `objects` as \"empty\" without checking whether it was actually `unreachable`. Each returned object carries its raw RPSL (`raw_rpsl`), parsed `attributes` (each value is an array - RPSL attributes like `members`/`mnt-by`/`remarks` legitimately repeat), the object's OWN `source:` attribute (`source_attr`) alongside which registry you asked (`source`), and `source_authority` flagging any mismatch between the two. `history` is accepted but NOT supported in this build (no NRTM-fed mirror exists) - passing it returns an explicit `history_note` rather than silently ignoring it or fabricating history; the response is always the object as-of right now. `matched_count`/`returned`/`truncated` bound a large result (an inverse origin lookup on a big network can be thousands of route objects) - `cursor` is always null, since a single whois query answers in one response with nothing to page yet.","description":"Look up IRR/RPSL objects (route, route6, aut-num, as-set) directly from the public IRR registries - RADB, RIPE, APNIC, ARIN, LEVEL3, NTTCOM - live, on demand (short-TTL cache; not a bulk mirror). Give ONE of: `prefix` (route/route6 exact-match), `asn` (aut-num plus an inverse origin lookup across route/route6), or `object_name` (an as-set/route-set/aut-num/maintainer primary key, e.g. \"AS-EXAMPLE\"). EVERY requested source is queried INDEPENDENTLY and every object it returns is kept separate - two sources both holding the same name are NEVER merged, because IRR data is genuinely contradictory across registries (measured live: RADB and RIPE hold two different \"AS-GOOGLE\" as-set objects with different members). `source_status` reports what happened for EACH requested source on EACH sub-query - `ok` (answered with data), `not_found` (answered \"no entries\", a real positive answer), `error` (answered but the reply was rejected), or `unreachable` (did not answer at all, e.g. connect timeout) - these are different facts and are never collapsed into one (#67); do not read a `not_found` on one source as evidence the object doesn't exist anywhere, and do not read a source missing from `objects` as \"empty\" without checking whether it was actually `unreachable`. Each returned object carries its raw RPSL (`raw_rpsl`), parsed `attributes` (each value is an array - RPSL attributes like `members`/`mnt-by`/`remarks` legitimately repeat), the object's OWN `source:` attribute (`source_attr`) alongside which registry you asked (`source`), and `source_authority` flagging any mismatch between the two. `history` is accepted but NOT supported in this build (no NRTM-fed mirror exists) - passing it returns an explicit `history_note` rather than silently ignoring it or fabricating history; the response is always the object as-of right now. `matched_count`/`returned`/`truncated` bound a large result (an inverse origin lookup on a big network can be thousands of route objects) - `cursor` is always null, since a single whois query answers in one response with nothing to page yet.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"Look up this ASN's aut-num plus every route/route6 object with it as origin (inverse lookup)."},"prefix":{"type":"string","description":"IPv4 or IPv6 CIDR, e.g. \"8.8.8.0/24\" or \"2001:4860::/32\". Exact-match route/route6 lookup."},"object_name":{"type":"string","description":"Exact primary key of an as-set, route-set, aut-num (\"AS15169\") or maintainer object."},"object_classes":{"type":"array","items":{"type":"string","enum":["route","route6","aut-num","as-set","route-set","mntner"]},"description":"Restrict which object classes are queried/returned. Omit for a sensible default based on which of asn/prefix/object_name you gave."},"sources":{"type":"array","items":{"type":"string","enum":["RADB","RIPE","APNIC","ARIN","LEVEL3","NTTCOM"]},"description":"Omit for the conservative default: all six, queried independently. LEVEL3 has been observed unreachable in testing - kept in the source list regardless (#554), and its status is reported per-call, never assumed."},"history":{"type":"boolean","default":false,"description":"NOT SUPPORTED in this build - returns an explicit unsupported note. See tool description."},"cursor":{"type":"string","description":"Reserved for future paging. This build always returns cursor: null."},"limit":{"type":"integer","default":100,"maximum":500}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/expand_as_set":{"post":{"operationId":"expand_as_set","summary":"Recursively expand an AS-SET (or route-set) into every member ASN, from the public IRR registries, with a HARD depth limit, a hard member-count limit, and cycle detection - real as-sets nest, and some are self-referential. FAIL-CLOSED, per #554: `complete` is false, with an explicit `incompleteness_reasons` list, whenever the depth limit truncated a branch, the member limit was reached (further ASNs are NOT added once hit - never silently capped and called done), a nested set could not be resolved from any requested source, or a source query errored/was unreachable. A caller MUST check `complete` before treating `asns` as authoritative for anything filter-shaped - an incomplete expansion that looks like a normal-sized result is exactly the case that black-holes legitimate traffic if used to build a prefix filter (this build does not generate filters - #554 slice 1/2 only). A CYCLE is reported in `cycles` but, once detected, is NOT itself a reason for `complete: false` - the traversal simply does not re-descend into an ancestor, which is the correct, complete answer for a self-referential set (verified against a synthetic 3-cycle fixture; confirmed live against real registries that none of the as-sets sampled while building this - AS-EXAMPLE, AS-HURRICANE, AS-NTT, AS-GTTCUST, AS-TELIANET/AS1299 - contain one, so a live-observed cycle example does not exist yet in this dataset). MEASURED LIVE 2026-08-25: real customer-cone as-sets are large - GTT's AS-GTTCUST alone has 500+ direct member ASNs AND ~766 nested per-peer sets in ONE object; Arelion/Twelve99's AS-TELIANET reached 2,436 further nested sets just three levels down. Expanding either FULLY would run for a projected tens of minutes to hours even at a good-citizen one-query-per-second-per-host pace - this tool bounds the work instead of attempting it, and `query_count` on every response tells you exactly how many registry queries THIS call made. `sources` is a PRECEDENCE list, not a fan-out: for each nested set, the first source in the list that has the object is used (mirrors the whois `-s SOURCE1,SOURCE2` convention) - this tool does not query every source for every node, unlike get_irr_objects, which does. `address_family` is accepted but informational only in this build (as-set membership is not family-specific in RPSL); see `address_family_note` in the response.","description":"Recursively expand an AS-SET (or route-set) into every member ASN, from the public IRR registries, with a HARD depth limit, a hard member-count limit, and cycle detection - real as-sets nest, and some are self-referential. FAIL-CLOSED, per #554: `complete` is false, with an explicit `incompleteness_reasons` list, whenever the depth limit truncated a branch, the member limit was reached (further ASNs are NOT added once hit - never silently capped and called done), a nested set could not be resolved from any requested source, or a source query errored/was unreachable. A caller MUST check `complete` before treating `asns` as authoritative for anything filter-shaped - an incomplete expansion that looks like a normal-sized result is exactly the case that black-holes legitimate traffic if used to build a prefix filter (this build does not generate filters - #554 slice 1/2 only). A CYCLE is reported in `cycles` but, once detected, is NOT itself a reason for `complete: false` - the traversal simply does not re-descend into an ancestor, which is the correct, complete answer for a self-referential set (verified against a synthetic 3-cycle fixture; confirmed live against real registries that none of the as-sets sampled while building this - AS-EXAMPLE, AS-HURRICANE, AS-NTT, AS-GTTCUST, AS-TELIANET/AS1299 - contain one, so a live-observed cycle example does not exist yet in this dataset). MEASURED LIVE 2026-08-25: real customer-cone as-sets are large - GTT's AS-GTTCUST alone has 500+ direct member ASNs AND ~766 nested per-peer sets in ONE object; Arelion/Twelve99's AS-TELIANET reached 2,436 further nested sets just three levels down. Expanding either FULLY would run for a projected tens of minutes to hours even at a good-citizen one-query-per-second-per-host pace - this tool bounds the work instead of attempting it, and `query_count` on every response tells you exactly how many registry queries THIS call made. `sources` is a PRECEDENCE list, not a fan-out: for each nested set, the first source in the list that has the object is used (mirrors the whois `-s SOURCE1,SOURCE2` convention) - this tool does not query every source for every node, unlike get_irr_objects, which does. `address_family` is accepted but informational only in this build (as-set membership is not family-specific in RPSL); see `address_family_note` in the response.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"as_set":{"type":"string","description":"e.g. \"AS-EXAMPLE\""},"sources":{"type":"array","items":{"type":"string","enum":["RADB","RIPE","APNIC","ARIN","LEVEL3","NTTCOM"]},"description":"Precedence order. Omit for the default: all six, RADB first."},"max_depth":{"type":"integer","default":10,"maximum":20},"max_members":{"type":"integer","default":10000,"maximum":50000},"address_family":{"type":"integer","description":"4 or 6. Informational only - see tool description."}},"required":["as_set"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/compare_bgp_irr_rpki":{"post":{"operationId":"compare_bgp_irr_rpki","summary":"Puts FOUR independent evidence classes side by side for an ASN or prefix (#554): what is actually ANNOUNCED (announced_prefix, real BGP RIB data), what IRR SAYS (live route/route6 objects via the same registries get_irr_objects queries), what RPKI SAYS (announced_prefix_rpki's bulk nightly validation, or an ad hoc rpki_roa coverage check when the prefix is not currently announced), and who the RIR DELEGATED FILES SAY the space belongs to (rir_allocation/rir_org - `matching` / `sibling` / `unrelated` / `unknown` relative to the registry organisation holding the origin AS). The product is the disagreement matrix: `matrix.all_agree` (the three-class bucket: announced, IRR origin match, RPKI valid), `matrix.announced_no_irr` (on the wire, no registered route object), `matrix.irr_not_announced` (registered in IRR, not seen on the wire), `matrix.rpki_invalid` (announced and RPKI-invalid) - each a `{count, prefixes}` list - plus `matrix.rir_matching` / `rir_sibling` / `rir_unrelated` / `rir_unknown` and `matrix.all_four_agree`, which carry `{count, cidrs}` (cidrs only; the full per-prefix evidence is always in `prefixes[].rir`). NONE of these buckets is mutually exclusive by design (an announced prefix can be BOTH missing from IRR AND rpki-invalid at once - that is two real facts, not a contradiction). Every row in `prefixes[]` keeps all four verdicts independently (`announced`, `irr`, `rpki_state`, `rir`) rather than collapsing them. NEVER CALL A MISMATCH A HIJACK: a prefix announced by an AS other than its allocation holder is usually a legitimate delegation, transfer or customer announcement, and `rir_unrelated` means only 'these two registry records do not identify the same organisation to us' - `rir_coverage.interpretation_note` restates this on every response. The RIR org resolution goes through the same corroborated resolver get_asn_allocations and lookup_rir_org use (#508/#489), and a `sibling` verdict is a byte-identical registered name under a DIFFERENT org id - a disclosure carrying `confidence: 'low'`, never a merge. Give ONE of `asn` (samples its announced prefixes, bounded by `sample_size` - default 200, max 2000; `announced_sample.sample_truncated` says if the sample is partial; the IRR side is a SINGLE inverse-origin query per source regardless of ASN size, same as get_irr_objects) or `prefix` (exact-match on both sides; if the prefix is not currently announced, RPKI falls back to an ad hoc rpki_roa coverage check, flagged in `rpki_coverage`). #67 applies throughout: `irr_coverage.query_complete` is false whenever a requested IRR source did not answer ok/not_found or the IRR result was truncated (`irr_coverage.incompleteness_reasons` says which); an announced prefix's `rpki_state` is `not_validated` when no row exists in announced_prefix_rpki (the nightly job has not scored it yet), which is NEVER the same fact as `notfound` (scored, no covering ROA exists); and `prefixes[].rir.status` has FOUR values that are never collapsed - `resolved` (a delegation record was read), `no_allocation_record` (checked, none covers this prefix), `lookup_failed` (the query failed, so it is unknown rather than absent) and `not_checked` (beyond `rir_sample_size`, unparseable, or `include_rir_allocation: false` - never queried). Do not treat a clean-looking matrix as complete without checking `announced_sample.sample_truncated`, `irr_coverage.query_complete` and `rir_coverage.sample_truncated`/`not_checked` first.","description":"Puts FOUR independent evidence classes side by side for an ASN or prefix (#554): what is actually ANNOUNCED (announced_prefix, real BGP RIB data), what IRR SAYS (live route/route6 objects via the same registries get_irr_objects queries), what RPKI SAYS (announced_prefix_rpki's bulk nightly validation, or an ad hoc rpki_roa coverage check when the prefix is not currently announced), and who the RIR DELEGATED FILES SAY the space belongs to (rir_allocation/rir_org - `matching` / `sibling` / `unrelated` / `unknown` relative to the registry organisation holding the origin AS). The product is the disagreement matrix: `matrix.all_agree` (the three-class bucket: announced, IRR origin match, RPKI valid), `matrix.announced_no_irr` (on the wire, no registered route object), `matrix.irr_not_announced` (registered in IRR, not seen on the wire), `matrix.rpki_invalid` (announced and RPKI-invalid) - each a `{count, prefixes}` list - plus `matrix.rir_matching` / `rir_sibling` / `rir_unrelated` / `rir_unknown` and `matrix.all_four_agree`, which carry `{count, cidrs}` (cidrs only; the full per-prefix evidence is always in `prefixes[].rir`). NONE of these buckets is mutually exclusive by design (an announced prefix can be BOTH missing from IRR AND rpki-invalid at once - that is two real facts, not a contradiction). Every row in `prefixes[]` keeps all four verdicts independently (`announced`, `irr`, `rpki_state`, `rir`) rather than collapsing them. NEVER CALL A MISMATCH A HIJACK: a prefix announced by an AS other than its allocation holder is usually a legitimate delegation, transfer or customer announcement, and `rir_unrelated` means only 'these two registry records do not identify the same organisation to us' - `rir_coverage.interpretation_note` restates this on every response. The RIR org resolution goes through the same corroborated resolver get_asn_allocations and lookup_rir_org use (#508/#489), and a `sibling` verdict is a byte-identical registered name under a DIFFERENT org id - a disclosure carrying `confidence: 'low'`, never a merge. Give ONE of `asn` (samples its announced prefixes, bounded by `sample_size` - default 200, max 2000; `announced_sample.sample_truncated` says if the sample is partial; the IRR side is a SINGLE inverse-origin query per source regardless of ASN size, same as get_irr_objects) or `prefix` (exact-match on both sides; if the prefix is not currently announced, RPKI falls back to an ad hoc rpki_roa coverage check, flagged in `rpki_coverage`). #67 applies throughout: `irr_coverage.query_complete` is false whenever a requested IRR source did not answer ok/not_found or the IRR result was truncated (`irr_coverage.incompleteness_reasons` says which); an announced prefix's `rpki_state` is `not_validated` when no row exists in announced_prefix_rpki (the nightly job has not scored it yet), which is NEVER the same fact as `notfound` (scored, no covering ROA exists); and `prefixes[].rir.status` has FOUR values that are never collapsed - `resolved` (a delegation record was read), `no_allocation_record` (checked, none covers this prefix), `lookup_failed` (the query failed, so it is unknown rather than absent) and `not_checked` (beyond `rir_sample_size`, unparseable, or `include_rir_allocation: false` - never queried). Do not treat a clean-looking matrix as complete without checking `announced_sample.sample_truncated`, `irr_coverage.query_complete` and `rir_coverage.sample_truncated`/`not_checked` first.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"},"prefix":{"type":"string","description":"IPv4 or IPv6 address or CIDR, e.g. \"8.8.8.0/24\""},"sources":{"type":"array","items":{"type":"string","enum":["RADB","RIPE","APNIC","ARIN","LEVEL3","NTTCOM"]},"description":"Omit for the default: all six, queried independently, same as get_irr_objects."},"sample_size":{"type":"integer","default":200,"maximum":2000,"description":"ASN mode only - how many of the ASN's announced prefixes to pull for comparison. Does not bound the IRR side (one inverse-origin query per source regardless of ASN size)."},"include_rir_allocation":{"type":"boolean","default":true,"description":"The fourth evidence class. Set false to skip the covering-allocation lookups entirely; every rir.status then reads not_checked with a reason, never an empty or absent result that could be mistaken for \"no allocation\"."},"rir_sample_size":{"type":"integer","default":500,"maximum":2000,"description":"How many of the comparison's prefixes get a covering-allocation lookup, announced rows first then by cidr. Measured cost: ~0.35-0.82 ms per prefix batched (2,000 prefixes = ~1.6 s). Prefixes beyond it are rir.status not_checked, and rir_coverage.sample_truncated says so."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/generate_prefix_filter":{"post":{"operationId":"generate_prefix_filter","summary":"SAFETY-CRITICAL: generates a router-pasteable prefix list for an AS-SET or a single ASN, sourced from live IRR route/route6 objects. PREFERS REFUSING OVER GUESSING (#554) - `refused: true` with no `filter` field and explicit `refusal_reasons` whenever: the AS-SET expansion (expand_as_set) is not `complete`; the resolved member-ASN count exceeds `max_asns` (default 50, hard ceiling 100 - refuses rather than silently truncating the ASN list); ANY requested IRR source did not answer ok/not_found for ANY member ASN, or that ASN's route lookup was truncated (irrObjectLookup's own 500-object cap - a single very large network, e.g. a big transit provider, WILL refuse under this, measured live against AS13335); or the resulting prefix list would be empty (an empty filter is a deny-all on most routers) unless `allow_empty: true` is passed explicitly. A refusal still returns full `provenance` - which registries, what each answered, when - so a human can see exactly what was tried. On success, `filter.prefixes` is EXACTLY what IRR registers - no ge/le broadening to shorter/longer masks is applied (a router-vendor convention, e.g. bgpq4's default \"up to /24\", that this tool does not assume on your behalf; see `note_no_ge_le`). Default `sources` excludes LEVEL3 (measured chronically unreachable, #554) so this tool is not refusing by default on a registry that never answers; pass it explicitly to include it, which also makes the call fail-closed on it like any other requested source. Every call, success or refusal, is logged to `prefix_filter_generation_run` (migration 191) for audit. `human_review_required: true` and `human_review_note` are always present, refused or not - THIS OUTPUT REQUIRES HUMAN REVIEW BEFORE USE ON A PRODUCTION ROUTER.","description":"SAFETY-CRITICAL: generates a router-pasteable prefix list for an AS-SET or a single ASN, sourced from live IRR route/route6 objects. PREFERS REFUSING OVER GUESSING (#554) - `refused: true` with no `filter` field and explicit `refusal_reasons` whenever: the AS-SET expansion (expand_as_set) is not `complete`; the resolved member-ASN count exceeds `max_asns` (default 50, hard ceiling 100 - refuses rather than silently truncating the ASN list); ANY requested IRR source did not answer ok/not_found for ANY member ASN, or that ASN's route lookup was truncated (irrObjectLookup's own 500-object cap - a single very large network, e.g. a big transit provider, WILL refuse under this, measured live against AS13335); or the resulting prefix list would be empty (an empty filter is a deny-all on most routers) unless `allow_empty: true` is passed explicitly. A refusal still returns full `provenance` - which registries, what each answered, when - so a human can see exactly what was tried. On success, `filter.prefixes` is EXACTLY what IRR registers - no ge/le broadening to shorter/longer masks is applied (a router-vendor convention, e.g. bgpq4's default \"up to /24\", that this tool does not assume on your behalf; see `note_no_ge_le`). Default `sources` excludes LEVEL3 (measured chronically unreachable, #554) so this tool is not refusing by default on a registry that never answers; pass it explicitly to include it, which also makes the call fail-closed on it like any other requested source. Every call, success or refusal, is logged to `prefix_filter_generation_run` (migration 191) for audit. `human_review_required: true` and `human_review_note` are always present, refused or not - THIS OUTPUT REQUIRES HUMAN REVIEW BEFORE USE ON A PRODUCTION ROUTER.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"as_set":{"type":"string","description":"e.g. \"AS-EXAMPLE\". Mutually exclusive with asn."},"asn":{"type":"integer","description":"A single origin ASN. Mutually exclusive with as_set."},"sources":{"type":"array","items":{"type":"string","enum":["RADB","RIPE","APNIC","ARIN","LEVEL3","NTTCOM"]},"description":"Omit for the default: every KNOWN-RELIABLE source (excludes LEVEL3). Any source passed explicitly is fail-closed - the whole call refuses if it does not answer cleanly for every member ASN."},"address_family":{"type":"integer","description":"4 or 6 - restrict output to that family only. Omit for both."},"allow_empty":{"type":"boolean","default":false,"description":"Explicit opt-in to accept a filter with zero prefixes instead of refusing."},"max_asns":{"type":"integer","default":50,"maximum":100,"description":"Per-call member-ASN ceiling. Exceeding it refuses rather than truncating the ASN list."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_bgp_communities":{"post":{"operationId":"get_bgp_communities","summary":"BGP community reference: definitions grouped by well-known (RFC-registered), operator-specific, IXP/route-server-specific, and historical (out of validity window). BGPScout holds ONLY the RFC well-known set today - operator_specific and ixp_specific are empty for every ASN, and `operator_specific_note` says so explicitly when you pass `asn` (absence of documentation, never evidence a network has no community policy). Every definition carries its source_url/source_type/confidence and validity window - nothing here is inferred from the community's numbers.","description":"BGP community reference: definitions grouped by well-known (RFC-registered), operator-specific, IXP/route-server-specific, and historical (out of validity window). BGPScout holds ONLY the RFC well-known set today - operator_specific and ixp_specific are empty for every ASN, and `operator_specific_note` says so explicitly when you pass `asn` (absence of documentation, never evidence a network has no community policy). Every definition carries its source_url/source_type/confidence and validity window - nothing here is inferred from the community's numbers.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"Filter operator-specific/ixp-specific definitions to this ASN (well-known definitions are always global and always included)."},"kind":{"type":"string","description":"standard | large | extended"},"direction":{"type":"string","description":"inbound | outbound | both | unknown"},"category":{"type":"string","description":"blackhole | export | route_server | informational | relationship | prepend | location | local_preference | unknown"},"date":{"type":"string","description":"ISO-8601. Ask what was valid on this date instead of now, for a historical lookup."}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/decode_bgp_communities":{"post":{"operationId":"decode_bgp_communities","summary":"Decode a list of BGP communities (standard \"A:B\", large \"A:B:C\" RFC 8092, or extended \"rt:GA:LA\"/\"soo:GA:LA\"/six raw hex bytes RFC 4360), one result per input, preserving order. Never invents a meaning from the numbers: each result is either a cited definition (source_url, source_type, confidence, validity window) or an explicit `status: 'unknown'`/`'invalid'` with a `reason` - most commonly \"No documentation on file for AS<n>'s communities\", because BGPScout holds no per-ASN community documentation today (#553 phase 1 - RFC well-known set only). `ambiguous: true` when multiple equally-authoritative definitions exist; never silently picks one. `context_match` says whether the community's own candidate-owner ASN agrees with `context_asn`, but the input is never rewritten by that context. Four-byte ASNs (large communities, and rt:/soo: with a Global Administrator > 65535, auto-selecting the RFC 5668 four-octet-AS encoding) round-trip exactly.","description":"Decode a list of BGP communities (standard \"A:B\", large \"A:B:C\" RFC 8092, or extended \"rt:GA:LA\"/\"soo:GA:LA\"/six raw hex bytes RFC 4360), one result per input, preserving order. Never invents a meaning from the numbers: each result is either a cited definition (source_url, source_type, confidence, validity window) or an explicit `status: 'unknown'`/`'invalid'` with a `reason` - most commonly \"No documentation on file for AS<n>'s communities\", because BGPScout holds no per-ASN community documentation today (#553 phase 1 - RFC well-known set only). `ambiguous: true` when multiple equally-authoritative definitions exist; never silently picks one. `context_match` says whether the community's own candidate-owner ASN agrees with `context_asn`, but the input is never rewritten by that context. Four-byte ASNs (large communities, and rt:/soo: with a Global Administrator > 65535, auto-selecting the RFC 5668 four-octet-AS encoding) round-trip exactly.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"communities":{"type":"array","items":{"type":"string"},"description":"e.g. [\"65535:666\", \"2914:1206\", \"55176:100:1\", \"rt:2914:100\"]"},"context_asn":{"type":"integer","description":"The ASN you observed these communities from/about, to check candidate-owner agreement. Does not change the parse."},"observed_at":{"type":"string","description":"ISO-8601. Match against definitions valid at this time instead of now."}},"required":["communities"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/bulk_lookup_asns":{"post":{"operationId":"bulk_lookup_asns","summary":"Summarize several ASNs at once (name, country, traffic, PoPs, IXP counts and domestic share) in one call. IXP fields are null with an ixp_data note where the ASN has no PeeringDB record - null means not surveyed, never zero exchanges (#366)","description":"Summarize several ASNs at once (name, country, traffic, PoPs, IXP counts and domestic share) in one call. IXP fields are null with an ixp_data note where the ASN has no PeeringDB record - null means not surveyed, never zero exchanges (#366)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asns":{"type":"array","items":{"type":"integer"}}},"required":["asns"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_top_networks":{"post":{"operationId":"get_top_networks","summary":"Top networks in a country ranked by a metric. by: traffic | prefixes | facilities | ixps","description":"Top networks in a country ranked by a metric. by: traffic | prefixes | facilities | ixps","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"by":{"type":"string"},"limit":{"type":"integer","default":20}},"required":["country"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_lead_reasons":{"post":{"operationId":"get_lead_reasons","summary":"Why an ASN is flagged as a sales lead - opportunity type and signal breakdown","description":"Why an ASN is flagged as a sales lead - opportunity type and signal breakdown","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/find_transport":{"post":{"operationId":"find_transport","summary":"Networks observed carrying long-haul traffic between two regions (transport/backbone providers). Filter by from/to country or city.","description":"Networks observed carrying long-haul traffic between two regions (transport/backbone providers). Filter by from/to country or city.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"from_country":{"type":"string"},"to_country":{"type":"string"},"from_city":{"type":"string"},"to_city":{"type":"string"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/lookup":{"post":{"operationId":"lookup","summary":"Everything we know about one IPv4 address, IPv6 address, CIDR or hostname, in a single call. Type is detected - no separate tool per input shape - and IPv6 is first-class. sections filters the payload: routing (covering prefix, origin ASN, more-specifics, bogon status - IANA special-use/reserved targets like documentation ranges resolve immediately, #480), geo (resolved location with EVERY candidate source and which one won), registration (RIR allocation, registered org with a method/confidence/evidence field - null with a reason rather than an unrelated org when the registry's opaque id is ambiguous across resource types, #489 - and contacts), rpki (Valid/Invalid/NotFound against the ROA set), rdns (PTR for an address; for a prefix, `forward_domains` is one row per registrable domain with a subdomain count and a `likely_wildcard` flag, so one wildcard record cannot crowd out every other domain and the payload stays small. The per-hostname `forward_records` list is OPT-IN via include_forward_records and is absent by default; `forward_records_included` says which you got, #488). Every section carries its own source and observation date so you can say how confident you are rather than presenting a guess as fact. Prefer this over lookup_ip and lookup_prefix, which answer narrower slices of the same question.","description":"Everything we know about one IPv4 address, IPv6 address, CIDR or hostname, in a single call. Type is detected - no separate tool per input shape - and IPv6 is first-class. sections filters the payload: routing (covering prefix, origin ASN, more-specifics, bogon status - IANA special-use/reserved targets like documentation ranges resolve immediately, #480), geo (resolved location with EVERY candidate source and which one won), registration (RIR allocation, registered org with a method/confidence/evidence field - null with a reason rather than an unrelated org when the registry's opaque id is ambiguous across resource types, #489 - and contacts), rpki (Valid/Invalid/NotFound against the ROA set), rdns (PTR for an address; for a prefix, `forward_domains` is one row per registrable domain with a subdomain count and a `likely_wildcard` flag, so one wildcard record cannot crowd out every other domain and the payload stays small. The per-hostname `forward_records` list is OPT-IN via include_forward_records and is absent by default; `forward_records_included` says which you got, #488). Every section carries its own source and observation date so you can say how confident you are rather than presenting a guess as fact. Prefer this over lookup_ip and lookup_prefix, which answer narrower slices of the same question.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target":{"type":"string","description":"IPv4, IPv6, CIDR (either family), or hostname"},"sections":{"type":"array","items":{"type":"string"},"description":"Any of: routing, geo, registration, rpki, rdns. Omit for all."},"include_forward_records":{"type":"boolean","default":false,"description":"Add the per-hostname rdns.forward_records list. Off by default because forward_domains already says the same thing for a fraction of the payload (#488). Turn it on only when you need individual hostnames."}},"required":["target"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_asn_geo_prefixes":{"post":{"operationId":"get_asn_geo_prefixes","summary":"Which address space an ASN has in a place, at PREFIX granularity - the inverse of find_networks_near (which goes place -> ASNs). Give an asn alone for its full per-location prefix breakdown, or add city/country or latitude+longitude+radius_km to constrain it. Returns the announced CIDRs with the geolocated IP count of each, not just totals. Coverage differs by field: coordinates and ip_count come from the per-prefix footprint (79,121 ASNs), while city/region/country come from the per-ASN city footprint (6,515 ASNs) - so an ASN can have located prefixes with NO city label, and a city filter cannot see it. An empty result means we have no geolocated space for that ASN, NEVER that the network is absent from the area. Coordinates are a 0.1-degree grid cell (~11km), not a precise centroid. Each row also carries an anycast verdict: is_anycast true means that prefix answers from many sites at once, so its coordinate is one sample, not a place the network is uniquely in - never report such a row as evidence of presence there; is_anycast null means unknown (a mixed-type ASN with no prefix-level anycast fact), not confirmed unicast.","description":"Which address space an ASN has in a place, at PREFIX granularity - the inverse of find_networks_near (which goes place -> ASNs). Give an asn alone for its full per-location prefix breakdown, or add city/country or latitude+longitude+radius_km to constrain it. Returns the announced CIDRs with the geolocated IP count of each, not just totals. Coverage differs by field: coordinates and ip_count come from the per-prefix footprint (79,121 ASNs), while city/region/country come from the per-ASN city footprint (6,515 ASNs) - so an ASN can have located prefixes with NO city label, and a city filter cannot see it. An empty result means we have no geolocated space for that ASN, NEVER that the network is absent from the area. Coordinates are a 0.1-degree grid cell (~11km), not a precise centroid. Each row also carries an anycast verdict: is_anycast true means that prefix answers from many sites at once, so its coordinate is one sample, not a place the network is uniquely in - never report such a row as evidence of presence there; is_anycast null means unknown (a mixed-type ASN with no prefix-level anycast fact), not confirmed unicast.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer","description":"ASN number"},"city":{"type":"string","description":"City name, substring match. Only matches ASNs with city-level data."},"country":{"type":"string","description":"ISO 2-letter code"},"latitude":{"type":"number"},"longitude":{"type":"number"},"radius_km":{"type":"integer","default":50,"description":"Used with latitude+longitude"},"min_ips":{"type":"integer","description":"Only prefixes contributing at least this many geolocated IPs at a location"},"limit":{"type":"integer","default":200}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_seller_profile":{"post":{"operationId":"get_seller_profile","summary":"Your sales org seller profile and its full colo footprint (#56): facility-level service areas, metro/city-level service areas, and the buildings PeeringDB already places the org in as read-only suggestions. A footprint declared only by metro is a real footprint - do not read an empty service_areas as \"none\".","description":"Your sales org seller profile and its full colo footprint (#56): facility-level service areas, metro/city-level service areas, and the buildings PeeringDB already places the org in as read-only suggestions. A footprint declared only by metro is a real footprint - do not read an empty service_areas as \"none\".","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/update_seller_profile":{"post":{"operationId":"update_seller_profile","summary":"Update your sales org seller profile (sells_transit/colocation/transport/ixp/consulting/cloud_connect booleans; transit_max_speed, transit_focus, transit_regions, target_market_size, notes)","description":"Update your sales org seller profile (sells_transit/colocation/transport/ixp/consulting/cloud_connect booleans; transit_max_speed, transit_focus, transit_regions, target_market_size, notes)","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sells_transit":{"type":"boolean"},"sells_colocation":{"type":"boolean"},"sells_transport":{"type":"boolean"},"sells_ixp":{"type":"boolean"},"sells_consulting":{"type":"boolean"},"sells_cloud_connect":{"type":"boolean"},"transit_max_speed":{"type":"string"},"transit_focus":{"type":"string"},"transit_regions":{"type":"string"},"target_market_size":{"type":"string"},"notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/run_sql":{"post":{"operationId":"run_sql","summary":"Execute a read-only SQL SELECT query (admin only). If your query already ends in its own LIMIT, that one is used and the `limit` argument is ignored; otherwise `limit` is appended. Either way results are capped at 200 rows, and a query whose own LIMIT exceeds that is rejected with a message saying so rather than silently truncated (#503).","description":"Execute a read-only SQL SELECT query (admin only). If your query already ends in its own LIMIT, that one is used and the `limit` argument is ignored; otherwise `limit` is appended. Either way results are capped at 200 rows, and a query whose own LIMIT exceeds that is rejected with a message saying so rather than silently truncated (#503).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string"},"limit":{"type":"integer","default":50,"description":"Ignored when the query ends in its own LIMIT clause."}},"required":["query"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_system_stats":{"post":{"operationId":"get_system_stats","summary":"Overall system statistics","description":"Overall system statistics","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_pipeline_history":{"post":{"operationId":"get_pipeline_history","summary":"Data-pipeline operations view (admin only) - the same three panels as /history: the last 20 data_refresh_log runs, IXP peer-count/speed changes between the latest two ix_peer_history snapshots, and ASN facility/IXP-count changes between the latest two asn_facility_history snapshots. Platform-wide, not scoped to any watchlist - distinct from get_dashboard_digest, which is the personal per-watched-ASN version of the facility/IXP change feed. Admin only (access_level >= 300); the web page also accepts a personal beta_features flag below that level, which this tool does not replicate.","description":"Data-pipeline operations view (admin only) - the same three panels as /history: the last 20 data_refresh_log runs, IXP peer-count/speed changes between the latest two ix_peer_history snapshots, and ASN facility/IXP-count changes between the latest two asn_facility_history snapshots. Platform-wide, not scoped to any watchlist - distinct from get_dashboard_digest, which is the personal per-watched-ASN version of the facility/IXP change feed. Admin only (access_level >= 300); the web page also accepts a personal beta_features flag below that level, which this tool does not replicate.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_sales_digest_preview":{"post":{"operationId":"get_sales_digest_preview","summary":"Admin-only preview of the weekly sales digest email: top-10 ASNs with lead_score >= 50, top-10 by boomerang_rate (routing-quality outliers, >15% with >= 5 recent traces), ASNs registered in the last 7 days, and recent new-IXP-join watchlist alerts. Same four queries as /sales-lab/digest-preview (#350). Every ingredient is separately available via other tools (get_buy_signals, get_boomerang_stats, get_new_networks, get_events) - this is the curated combined shape only.","description":"Admin-only preview of the weekly sales digest email: top-10 ASNs with lead_score >= 50, top-10 by boomerang_rate (routing-quality outliers, >15% with >= 5 recent traces), ASNs registered in the last 7 days, and recent new-IXP-join watchlist alerts. Same four queries as /sales-lab/digest-preview (#350). Every ingredient is separately available via other tools (get_buy_signals, get_boomerang_stats, get_new_networks, get_events) - this is the curated combined shape only.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_mcp_usage_stats":{"post":{"operationId":"get_mcp_usage_stats","summary":"MCP adoption and usage from mcp_call_log (#457): calls/day split by principal type (api_key vs oauth) over the trailing window, distinct active principals, tool call counts (including never-called tools, by diffing against the live tool set - a strong deprecation signal), client mix from the initialize handshake, and error/denial counts by tool. Data only exists from when mcp_call_log started being written; an empty or short history is a fresh table, not zero usage.","description":"MCP adoption and usage from mcp_call_log (#457): calls/day split by principal type (api_key vs oauth) over the trailing window, distinct active principals, tool call counts (including never-called tools, by diffing against the live tool set - a strong deprecation signal), client mix from the initialize handshake, and error/denial counts by tool. Data only exists from when mcp_call_log started being written; an empty or short history is a fresh table, not zero usage.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"integer","default":30,"description":"Trailing window in days, max 180"}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_data_coverage":{"post":{"operationId":"get_data_coverage","summary":"Data coverage scores per country [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/scoreboard on the site for the current caveats.]","description":"Data coverage scores per country [BETA: the underlying calculation is still settling - treat the result as directional, not yet safe to quote as a stated fact. See /traceroutes/analysis/scoreboard on the site for the current caveats.]","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"min_score":{"type":"integer","default":0}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_buy_signals":{"post":{"operationId":"get_buy_signals","summary":"Networks showing ready-to-buy signals, ranked by lead score (sales lab)","description":"Networks showing ready-to-buy signals, ranked by lead score (sales lab)","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"min_score":{"type":"integer","default":40},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_rpki_opportunities":{"post":{"operationId":"get_rpki_opportunities","summary":"Networks where RPKI ROV has NOT BEEN OBSERVED. Read the name as a legacy label: this is NOT a list of networks without RPKI. The source is bgp.tools's \"Validating RPKI ROV\" tag, which is positive evidence when set and silence when not - the measurement needs a RIPE Atlas probe behind the network, so most nulls are unmeasured rather than negative (21 of 623 Canadian ASNs are observed). It also says nothing about whether these networks publish ROAs for their own prefixes. The response carries `coverage` and a `signal_note`; quote them with any list you produce, and never describe a listed network as insecure or as not validating (#490).","description":"Networks where RPKI ROV has NOT BEEN OBSERVED. Read the name as a legacy label: this is NOT a list of networks without RPKI. The source is bgp.tools's \"Validating RPKI ROV\" tag, which is positive evidence when set and silence when not - the measurement needs a RIPE Atlas probe behind the network, so most nulls are unmeasured rather than negative (21 of 623 Canadian ASNs are observed). It also says nothing about whether these networks publish ROAs for their own prefixes. The response carries `coverage` and a `signal_note`; quote them with any list you produce, and never describe a listed network as insecure or as not validating (#490).","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"country":{"type":"string"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_industry_verticals":{"post":{"operationId":"get_industry_verticals","summary":"Networks by industry vertical with lead scores. vertical: isp | gov | cdn | corp | uni | biznet | mobile","description":"Networks by industry vertical with lead scores. vertical: isp | gov | cdn | corp | uni | biznet | mobile","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"vertical":{"type":"string"},"country":{"type":"string"},"limit":{"type":"integer","default":50}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/generate_outreach_templates":{"post":{"operationId":"generate_outreach_templates","summary":"Generate the personalized routing, peering and general outreach copy shown by /sales-lab/outreach for an ASN. Admin only, matching that page.","description":"Generate the personalized routing, peering and general outreach copy shown by /sales-lab/outreach for an ASN. Admin only, matching that page.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"asn":{"type":"integer"}},"required":["asn"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_org_customers":{"post":{"operationId":"get_org_customers","summary":"Auto-detected customers for your org, split by what the detection actually means. `customers` are networks downstream of the org's OWN ASNs - strength=observed is a traceroute-observed transit handoff (strongest), strength=adjacency is BGP AS_PATH only and MAY BE PEERING RATHER THAN TRANSIT. `nearby_networks` are ASNs at an IXP or facility the org linked: proximity, not commerce - prospects, not customers. An empty customers list means nothing was observed, not that the org has no customers.","description":"Auto-detected customers for your org, split by what the detection actually means. `customers` are networks downstream of the org's OWN ASNs - strength=observed is a traceroute-observed transit handoff (strongest), strength=adjacency is BGP AS_PATH only and MAY BE PEERING RATHER THAN TRANSIT. `nearby_networks` are ASNs at an IXP or facility the org linked: proximity, not commerce - prospects, not customers. An empty customers list means nothing was observed, not that the org has no customers.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_org_prospects":{"post":{"operationId":"get_org_prospects","summary":"Get performance targeting prospects","description":"Get performance targeting prospects","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_org_performance_targets":{"post":{"operationId":"get_org_performance_targets","summary":"The ranked Performance Targeting list from /prospect/performance (#350): precomputed improvement scores, TIER1 networks excluded (they don't buy transit), the org's own ASN excluded unconditionally (#432 - a company must never see itself as its own top lead), and not_interested-tagged leads hidden by default (hide_dismissed:false to include them, labelled). Requires the org's feature_performance_targeting flag - returns an error naming that if it is off, same as the web page's feature-disabled screen. Does NOT include the web page's geocoded radius search or its downstream-customer detection - see the response's not_included note. eligible_total/truncated describe the query's own cap honestly (#434) - a caller must not read `targets.length` as the whole population.","description":"The ranked Performance Targeting list from /prospect/performance (#350): precomputed improvement scores, TIER1 networks excluded (they don't buy transit), the org's own ASN excluded unconditionally (#432 - a company must never see itself as its own top lead), and not_interested-tagged leads hidden by default (hide_dismissed:false to include them, labelled). Requires the org's feature_performance_targeting flag - returns an error naming that if it is off, same as the web page's feature-disabled screen. Does NOT include the web page's geocoded radius search or its downstream-customer detection - see the response's not_included note. eligible_total/truncated describe the query's own cap honestly (#434) - a caller must not read `targets.length` as the whole population.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"hide_dismissed":{"type":"boolean","default":true},"limit":{"type":"integer","default":50,"maximum":500}}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_org_assets":{"post":{"operationId":"get_org_assets","summary":"Get linked ASNs, facilities, IXPs","description":"Get linked ASNs, facilities, IXPs","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/get_custom_facility":{"post":{"operationId":"get_custom_facility","summary":"One of your org's private (non-PeeringDB) custom facility locations (#384) - a telco central office, a headend, a private POP typed in rather than sourced from PeeringDB. Scoped to the caller's own org; a custom facility belonging to a different org returns 'not found', never another org's data. Distinct from get_facility_info, which only ever searches the public PeeringDB table and never sees these. Pass the id from the org's own list (routes/facilities.js's /facilities page under 'Your locations').","description":"One of your org's private (non-PeeringDB) custom facility locations (#384) - a telco central office, a headend, a private POP typed in rather than sourced from PeeringDB. Scoped to the caller's own org; a custom facility belonging to a different org returns 'not found', never another org's data. Distinct from get_facility_info, which only ever searches the public PeeringDB table and never sees these. Pass the id from the org's own list (routes/facilities.js's /facilities page under 'Your locations').","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"The org_custom_facility id (NOT a PeeringDB id - the two id spaces overlap numerically but are never interchangeable)."}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/delete_custom_facility":{"post":{"operationId":"delete_custom_facility","summary":"Delete one of your org's custom (non-PeeringDB) facility locations, and its service-area settings with it - the same delete button as the location's page (#350). A location belonging to a different org, or one that does not exist, both return the SAME 'not found' error.","description":"Delete one of your org's custom (non-PeeringDB) facility locations, and its service-area settings with it - the same delete button as the location's page (#350). A location belonging to a different org, or one that does not exist, both return the SAME 'not found' error.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"The org_custom_facility id, from get_custom_facility."}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/update_custom_facility_service_area":{"post":{"operationId":"update_custom_facility_service_area","summary":"Set the radius/on-net service-area settings for one of your org's custom facility locations - the same settings form as the location's page (#350). radius_km clamps to 1-2000 (default 100) and is ignored when on_net_only is true.","description":"Set the radius/on-net service-area settings for one of your org's custom facility locations - the same settings form as the location's page (#350). radius_km clamps to 1-2000 (default 100) and is ignored when on_net_only is true.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"integer","description":"The org_custom_facility id, from get_custom_facility."},"radius_km":{"type":"integer","default":100},"on_net_only":{"type":"boolean","default":false}},"required":["id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/set_service_area":{"post":{"operationId":"set_service_area","summary":"Add/update or remove a facility-level service area for your sales org and synchronize the seller-profile map projection. Requires Sales access.","description":"Add/update or remove a facility-level service area for your sales org and synchronize the seller-profile map projection. Requires Sales access.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"upsert | remove"},"facility_id":{"type":"integer"},"radius_km":{"type":"integer","default":200},"on_net_only":{"type":"boolean","default":false}},"required":["action","facility_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/update_org_asset":{"post":{"operationId":"update_org_asset","summary":"Link or unlink an ASN, facility or IXP to your organization. Requires the same level-250 access as /org and rejects read-only org members.","description":"Link or unlink an ASN, facility or IXP to your organization. Requires the same level-250 access as /org and rejects read-only org members.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"link | unlink"},"asset_type":{"type":"string","description":"asn | facility | ixp"},"id":{"type":"integer"}},"required":["action","asset_type","id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/update_org_customer":{"post":{"operationId":"update_org_customer","summary":"Acknowledge or dismiss an auto-detected customer belonging to your organization. Requires the same level-250 access as /org and rejects read-only org members. Acknowledging also tags the network `prospected` unless you or your org already tagged it something (#437) - the same side effect POST /org/ack-customer has.","description":"Acknowledge or dismiss an auto-detected customer belonging to your organization. Requires the same level-250 access as /org and rejects read-only org members. Acknowledging also tags the network `prospected` unless you or your org already tagged it something (#437) - the same side effect POST /org/ack-customer has.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"acknowledge | dismiss"},"id":{"type":"integer"}},"required":["action","id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/ack_all_org_customers":{"post":{"operationId":"ack_all_org_customers","summary":"Acknowledge every unacknowledged, undismissed auto-detected customer for your organization, and tag their underlying ASNs `prospected` (capped at 200 distinct ASNs, oldest-detected first - matching POST /org/ack-all exactly). A tag a human already chose for a network is never overwritten. Always succeeds, even with nothing left to acknowledge.","description":"Acknowledge every unacknowledged, undismissed auto-detected customer for your organization, and tag their underlying ASNs `prospected` (capped at 200 distinct ASNs, oldest-detected first - matching POST /org/ack-all exactly). A tag a human already chose for a network is never overwritten. Always succeeds, even with nothing left to acknowledge.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/add_org_member":{"post":{"operationId":"add_org_member","summary":"Invite an existing BGPScout user (by their exact account email) into your organization. Requires you to be an org admin or site admin. Only joins a user with no organization yet, or already in this one - never moves someone out of another org - and raises their access_level to at least 200 without ever lowering it. Always returns success regardless of whether the email matched a user or was eligible to join (matches POST /org/add-member's privacy behaviour - this cannot be used to enumerate registered emails).","description":"Invite an existing BGPScout user (by their exact account email) into your organization. Requires you to be an org admin or site admin. Only joins a user with no organization yet, or already in this one - never moves someone out of another org - and raises their access_level to at least 200 without ever lowering it. Always returns success regardless of whether the email matched a user or was eligible to join (matches POST /org/add-member's privacy behaviour - this cannot be used to enumerate registered emails).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","description":"Exact account email of the user to invite."}},"required":["email"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/remove_org_member":{"post":{"operationId":"remove_org_member","summary":"Remove a member from your organization by their user id. Requires you to be an org admin or site admin, and you cannot remove yourself. A user id that does not exist or belongs to a different org refuses with the identical message either way.","description":"Remove a member from your organization by their user id. Requires you to be an org admin or site admin, and you cannot remove yourself. A user id that does not exist or belongs to a different org refuses with the identical message either way.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer","description":"The target user's id (from get_org_assets/your members list), not an ASN."}},"required":["user_id"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/call/change_org_member_role":{"post":{"operationId":"change_org_member_role","summary":"Change a member's role within your organization (admin | member | readonly). Requires you to be an org admin or site admin, and you cannot change your own role (prevents locking yourself out). A user id that does not exist or belongs to a different org refuses with the identical message either way.","description":"Change a member's role within your organization (admin | member | readonly). Requires you to be an org admin or site admin, and you cannot change your own role (prevents locking yourself out). A user id that does not exist or belongs to a different org refuses with the identical message either way.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"integer"},"role":{"type":"string","description":"admin | member | readonly"}},"required":["user_id","role"]}}}},"responses":{"200":{"description":"Tool result","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"X-API-Key"},"BearerAuth":{"type":"http","scheme":"bearer"}}},"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}]}