On this page
QuickstartIntroductionData quality & methodologyAPI key authenticationRate limitsMCP serverMCP authenticationTool catalogueExample promptsClaudeChatGPTCursorOpenWebUIGET /api/traceroutes/statsPOST /api/traceroutes/submitGET /api/asn/:asnGET /api/asn/:asn/neighboursGET /api/embed/asn/:asnAlert webhooksMTR clientStart hereOverview
The BGPScout API lets you query network data, retrieve ASN details, and submit traceroute results. All responses are JSON unless otherwise noted.
Base URL: https://bgpscout.io/api
Public endpoints need no credentials. Endpoints that write data - or that read your account's data - require an API key in the X-API-Key header.
How the data is madeData quality & methodology
Trace processing pipeline
Every traceroute submitted to BGPScout goes through a multi-stage enrichment pipeline before being included in any analysis or public results:
- Submission - raw MTR/traceroute data is received via the API or RIPE Atlas.
- Enrichment - each hop IP is resolved to an ASN (via BGP prefix tables), geolocated (via geofeed, IPinfo, MaxMind), and classified (anycast, IXP, cloud, etc.).
- Quality scoring - a 0-10 data quality score is computed from PTR record coverage, geo coverage, hop completion ratio, and path coherence.
- Display gating - traces scoring ≤ 2 are auto-rejected. Scores 3-5 go to pending review. Scores 6+ are auto-approved. Only approved traces appear in search results and analysis.
Unenriched traces are never shown. Traces awaiting enrichment (quality_score IS NULL) are excluded from all public views, search results, and analysis tools. You can see processing status on your submissions page.
Boomerang detection
A "boomerang route" is detected when a traceroute from country A to a destination also in country A transits a different country. That indicates suboptimal routing where domestic traffic unnecessarily crosses international borders.
Data sources
- Traceroutes - user-submitted MTR tests, RIPE Atlas measurements, automated probes in 30+ countries
- ASN data - ARIN, RIPE NCC, APNIC, LACNIC, AFRINIC registry data
- PeeringDB - facility presence, IXP memberships, port speeds
- GeoIP - geofeed (RFC 8805), IPinfo, MaxMind, PTR hostname analysis
- Domain intelligence - Certificate Transparency, DNS enumeration, WHOIS
AccessAuthentication
Some endpoints require an API key passed in the X-API-Key HTTP header. Public endpoints do not require authentication.
To obtain a key, log in and visit Account Settings. Organisation accounts can also issue org-wide keys from the org dashboard.
curl -H "X-API-Key: your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"traces": []}' \
https://bgpscout.io/api/traceroutes/submitKeys are only shown once, when created, and are stored hashed - if you lose one, revoke it and issue a new one. Never send a key in a query string: it ends up in access logs, proxy logs and Referer headers.
LimitsRate limits
REST API endpoints under /api are limited to 300 requests per 15-minute window, bucketed by client IP address. Exceeding the limit returns 429 with a JSON error body.
The MCP server at /mcp has its own, separate limiter - 60 requests per minute per API key (or per IP when unauthenticated). See MCP server.
Response headers
| Header | Description |
|---|---|
RateLimit-Policy | The active policy, e.g. 300;w=900 - 300 requests per 900-second window |
RateLimit-Limit | Maximum requests per window |
RateLimit-Remaining | Requests remaining in the current window |
RateLimit-Reset | Seconds until the limit resets |
429 response
{
"success": false,
"error": "Rate limit exceeded. Please slow down and try again later."
}
IntegrationsMCP server (Model Context Protocol)
BGPScout ships an MCP server, so an AI assistant - Claude, ChatGPT, Cursor, OpenWebUI - can query the platform directly: look up ASNs, search IXPs and facilities, read traceroutes, find prospects, and write back to your watchlist.
One endpoint: https://bgpscout.io/mcp
| What | Value |
|---|---|
| Protocol | MCP 2024-11-05 over HTTP (JSON-RPC 2.0). Methods: initialize, tools/list, tools/call, ping |
| Server identity | {"name": "bgpscout", "version": "1.0.0"} |
| Tools | 160 on a standard account. Org accounts see 14 more (174), admins 10 more again (184 in the full catalogue). Counted live from the tool registry on every render of this page. |
| Rate limit | 60 requests per minute, per API key (per IP when unauthenticated) |
HTTP surface
| Method & path | Auth | Purpose |
|---|---|---|
| POST /mcp | Yes | The MCP JSON-RPC endpoint. This is the URL you give an MCP client. |
| GET /mcp | No | Discovery - returns protocolVersion, capabilities and serverInfo so a client can confirm the server before authenticating. |
| GET /mcp/tools | Yes | The tool list as plain JSON, filtered to your access level. |
| POST /mcp/tools/call | Yes | Call a tool with {"name": "...", "arguments": {...}}. |
| POST /mcp/call/:tool | Yes | REST-style call - the JSON body is the tool's arguments. Used by OpenAPI clients. |
| GET /mcp/openapi.json | No | OpenAPI 3.1 spec, one operation per tool, for tool servers that speak OpenAPI rather than MCP. |
| GET /mcp/docs | No | Human-readable reference: every tool, grouped by category, with its description. Always current - the Tool catalogue below is generated from the same registry. |
| GET /mcp/info | No | Connection details as JSON - OAuth URLs, endpoints, sample client config. |
Authentication
Two methods, both accepted on the same endpoint:
| Method | How | Best for |
|---|---|---|
| API key | X-API-Key: KEY - or Authorization: Bearer KEY, which many clients send instead | Claude Code, Cursor, OpenWebUI, scripts |
| OAuth 2.0 | Browser login with Google or PeeringDB, then an explicit consent screen. PKCE (S256) is mandatory; dynamic client registration at /oauth/register | claude.ai connectors, ChatGPT |
Get a key from Account Settings. An unauthenticated call returns 401 with a WWW-Authenticate header pointing at the protected-resource metadata, which is what lets an OAuth-capable client start the login flow on its own.
The server deliberately does not accept a key in the query string (?api_key=…) - that leaks into access logs, proxy logs, browser history and Referer headers. Header only.
Check your key works
curl -s -X POST https://bgpscout.io/mcp \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Tool catalogue
The 160 tools are grouped into categories below, generated live from the same registry /mcp/docs renders - this list cannot drift from the server the way a hand-typed copy did (#258). The list_tools tool does the same from inside a conversation - call it first, optionally with a category, instead of loading everything.
Discovery
- list_tools
ASN lookup & compare
- lookup_asn
- search_asns
- bulk_lookup_asns
- compare_asns
- get_asn_history
- get_route_history
- get_bgp_state
- get_asn_neighbours
- get_asn_ixp_colocated
- get_asn_peersdeprecated
ASN presence
- get_asn_facilities
- get_asn_ixps
- get_asn_cdn_caches
- get_transit_providers
- find_ddos_protected
- get_asn_prefixes
- get_prefix_counts
- get_asn_ipv6_reachability
Peering recommendations
- get_recommended_ixps
- get_recommended_peers
- recommend_transit
IP & prefix (whois)
- lookup
- lookup_ip
- lookup_prefix
- get_asn_bogons
- get_asn_rpki_status
- get_rpki_validation_history
- get_rpki_comparison_runs
IRR / RPSL
- get_irr_objects
- expand_as_set
- compare_bgp_irr_rpki
- generate_prefix_filter
BGP communities
- get_bgp_communities
- decode_bgp_communities
Registry allocations
- search_allocations
- get_asn_allocations
- search_transfers
- lookup_rir_org
- search_dormant_ipv4
- get_dormancy_by_registry
Geographic search
- find_near
- find_networks_near
- get_asn_density
- get_asn_geo_prefixes
- find_transit_near
- find_upstreams_near
- find_networks_at_facilities_near
- find_networks_on_ixps_near
Change feed
- get_events
Growth & trends
- find_growing_networks
- get_asn_growth
IXP & facility
- get_ixp_info
- get_ixp_members
- get_facility_info
- get_facility_members
- get_facility_events
- find_near
CDN
- search_cdn_caches
Traceroutes & analysis
- search_traceroutes
- get_traceroute
- get_my_traceroutes
- get_asn_traces
- get_route_heatmap
- get_country_scoreboard
- get_tier1_comparison
- get_tier1_city_view
- get_asn_connectivity_profile
- get_common_routes
- get_boomerang_stats
- get_asn_routing_quality
- get_dashboard_summary
Relationships
- find_co_located
- find_transit_customers
- get_org_asns
Sales & prospecting
- find_prospects
- find_long_haul_customers
- find_transport
- get_cloud_connect_prospects
- get_prospect_tags
- get_clients
- get_search_templates
- generate_outreach_templates
- get_asn_lead_score
- get_lead_reasons
- get_buy_signals
- get_rpki_opportunities
- get_industry_verticals
Intelligence & ranking
- get_new_networks
- get_growing_networks
- get_prefix_growth
- get_country_benchmark
- get_top_networks
Domains
- domain_lookup
- search_domains
- get_domain_intel
- compare_domain_scans
Exports
- create_export
- get_export
- list_exports
- cancel_export
On-demand measurements
- create_measurement
- get_measurement
- list_measurements
- cancel_measurement
Your saved data
- get_watchlist
- get_alerts
- get_notifications
- get_saved_searches
- get_auto_scout
- get_favorites
- get_trace_favorites
- get_dashboard_pins
- get_dashboard_digest
- list_alert_destinations
- get_alert_deliveries
Org & seller
- get_org_customers
- get_org_prospects
- get_org_performance_targets
- get_org_assets
- get_seller_profile
- get_custom_facility
Actions (write)
- add_to_watchlist
- remove_from_watchlist
- add_ixp_to_watchlist
- remove_ixp_from_watchlist
- add_facility_to_watchlist
- remove_facility_from_watchlist
- add_org_to_watchlist
- remove_org_from_watchlist
- pin_to_dashboard
- unpin_from_dashboard
- add_favorite
- remove_favorite
- add_trace_favorite
- remove_trace_favorite
- report_data_issue
- list_watch_rule_types
- create_watch_rule
- preview_watch_rule
- list_watch_rules
- delete_watch_rule
- set_watch_rule_active
- update_watch_rule
- trigger_domain_scan
- rerun_domain_scan
- delete_domain_scan
- tag_prospect
- untag_prospect
- undismiss_prospect
- save_search
- delete_saved_search
- create_auto_scout
- set_auto_scout_frequency
- delete_auto_scout
- create_client
- add_asn_to_client
- remove_asn_from_client
- delete_client
- mark_alerts_read
- archive_alerts
- dismiss_notification
- archive_trace_batch
- unarchive_trace_batch
- exclude_trace_batch_from_transit
- update_seller_profile
- set_service_area
- delete_custom_facility
- update_custom_facility_service_area
- update_org_asset
- update_org_customer
- ack_all_org_customers
- add_org_member
- remove_org_member
- change_org_member_role
- create_alert_destination
- update_alert_destination
- delete_alert_destination
- test_alert_destination
A deprecated tag marks an old name kept only as an alias - get_asn_peers despite its name returns IXP co-presence, not observed peers; use get_asn_neighbours for BGP adjacency (#259).
Three more tools appear when your account belongs to an organisation, and 10 admin-only tools exist beyond that (not listed above - internal operational tools, not part of the documented public surface). Everything is access-checked server-side, so a tool you cannot use never appears in your list.
Focusing the tool list
160 tools is a lot of context for one conversation. The server publishes curated subsets you can paste into a client that supports a function-name filter (OpenWebUI, for example), generated from the same TOOL_PRESETS the server itself uses:
| Preset | Tools |
|---|---|
| whois & lookup | lookup_asn
lookup_ip
lookup_prefix
get_asn_bogons
get_asn_prefixes
get_asn_facilities
get_asn_ixps
search_asns
get_facility_members
find_near
search_allocations
get_asn_allocations
search_transfers
lookup_rir_org |
| who is in this area | find_networks_near
find_transit_near
find_upstreams_near
find_networks_at_facilities_near
find_networks_on_ixps_near
find_near
lookup_ip
get_facility_members
get_ixp_members |
| growth prospecting | find_growing_networks
get_asn_growth
find_networks_at_facilities_near
find_networks_on_ixps_near
lookup_asn
get_events |
| sales | lookup_asn
search_asns
find_prospects
find_ddos_protected
get_lead_reasons
get_buy_signals
get_top_networks
find_transport
tag_prospect
create_client
add_asn_to_client
search_dormant_ipv4 |
| network ops / research | lookup_asn
lookup_ip
get_asn_history
get_route_history
get_bgp_state
get_asn_neighbours
get_transit_providers
find_upstreams_near
search_traceroutes
get_route_heatmap
get_tier1_comparison
get_tier1_city_view
get_country_scoreboard
compare_asns
recommend_transit |
Example prompts
Once the connector is live, ask in plain language. These are real questions the tool set answers:
"Look up AS577, then compare it with AS812 and AS6327 - who has the widest IXP footprint?"
lookup_asn, compare_asns, get_asn_ixps"Which networks peer at an exchange within 100 km of Montréal but have no data-centre presence there?"
find_networks_on_ixps_near, find_networks_at_facilities_near"Who does AS13335 buy transit from, and which of those handoffs did we actually see in a traceroute?"
get_transit_providers"Who peers with AS33185, and in which direction?"
get_asn_neighbours"Show me Canadian networks that added the most facilities in the last 90 days, then tag the top three as prospects."
find_growing_networks, tag_prospect"Did AS16509 buy its address space or was it allocated directly? Show any transfers."
get_asn_allocations, search_transfersTool results carry their own caveats - coverage limits, what an observation does and does not prove - and the assistant will repeat them. That is deliberate: a BGP adjacency is not proof of a commercial relationship, and "not observed" is not "does not exist".
Claude
Claude Code (CLI)
Add the server once, with your API key as a header:
claude mcp add --transport http bgpscout https://bgpscout.io/mcp \
--header "X-API-Key: YOUR_API_KEY"
# confirm it registered
claude mcp list
claude mcp get bgpscoutThe default scope is the current project. Add --scope user to make it available in all your projects, or --scope project to share it with your team through a checked-in .mcp.json. Inside a session, /mcp shows connection status.
Claude Code with OAuth instead of a key
Register the server without a header, then authenticate in the browser - useful if you would rather not store a key in a config file:
claude mcp add --transport http bgpscout https://bgpscout.io/mcp
# then, inside Claude Code:
# /mcp → select "bgpscout" → Authenticateclaude.ai and Claude Desktop (custom connector)
- Open Settings → Connectors.
- Click Add custom connector.
- Paste the URL
https://bgpscout.io/mcpand click Add. - Click Connect. You are sent to BGPScout to sign in with Google or PeeringDB, then back to Claude with the tools enabled.
Use the OAuth flow here rather than a custom header. Header auth in claude.ai connectors is a gradual rollout and, as of July 2026, headers are not reliably forwarded on every tool call - OAuth is the dependable path. On Team and Enterprise plans an owner adds the connector from Admin settings → Connectors and members then connect to it.
ChatGPT
To connect BGPScout to ChatGPT as an MCP app:
- Go to ChatGPT Settings → Apps → Add App.
- Set Name to
bgpscout.io. - Set MCP Server URL to
https://bgpscout.io/mcp. - Set Authentication to
OAuth. - Leave Registration method on Dynamic Client Registration. BGPScout implements RFC 7591 at
/oauth/registerand advertises it in its metadata, so the client registers itself and is issued a client ID automatically - there is no fixed ID to paste. - Only if your client cannot do dynamic registration, register once by hand and use the ID it returns:
The response contains thecurl -X POST https://bgpscout.io/oauth/register \ -H 'Content-Type: application/json' \ -d '{"client_name":"My Client","redirect_uris":["https://example.com/callback"]}'client_idto use below. Client secrets are not issued: this is a public client, sotoken_endpoint_auth_methodisnoneand PKCE (S256) is mandatory. - The remaining fields:
OAuth Client ID From dynamic registration, or the client_idreturned aboveOAuth Client Secret Leave empty Token endpoint auth method noneAuth URL https://bgpscout.io/oauth/authorizeToken URL https://bgpscout.io/oauth/token - Check "I understand and want to continue", then click Create.
The first time you use the BGPScout app in ChatGPT you are redirected to BGPScout to log in with Google or PeeringDB. After authorising, you land back in ChatGPT with the MCP tools available.
OAuth endpoints reference
| Purpose | URL |
|---|---|
| Authorization | https://bgpscout.io/oauth/authorize |
| Token exchange | https://bgpscout.io/oauth/token |
| Dynamic registration | https://bgpscout.io/oauth/register |
| Protected resource metadata | https://bgpscout.io/.well-known/oauth-protected-resource |
| Authorization server metadata | https://bgpscout.io/.well-known/oauth-authorization-server |
Grant types are authorization_code and refresh_token. PKCE is mandatory - the authorization endpoint requires a code_challenge with code_challenge_method=S256 and rejects anything else. Before a code is issued you see an explicit consent screen naming the application and the scopes it asked for; nothing is granted silently.
Cursor
Cursor reads .mcp.json. Add BGPScout with an API key header - the same block works in any config-file client:
{
"mcpServers": {
"bgpscout": {
"type": "http",
"url": "https://bgpscout.io/mcp",
"headers": {
"X-API-Key": "YOUR_API_KEY"
}
}
}
}Get the key from Account Settings. Cursor does not do the OAuth flow that ChatGPT and claude.ai use, so the header is the route here.
OpenWebUI
Add an OpenAPI tool server pointing at https://bgpscout.io/mcp (spec at /mcp/openapi.json), with auth set to Bearer and your API key as the token.
160 tools is a lot of context for one conversation. OpenWebUI supports a function-name filter, so paste one of the curated presets from the Tool catalogue into it to narrow what loads.
EndpointTraceroute statistics
Returns aggregate statistics about all collected traceroutes, plus a breakdown of the source countries they came from.
Example request
curl https://bgpscout.io/api/traceroutes/statsExample response
Live values, captured when this page was rendered.
{
"success": true,
"stats": {
"total_traces": 283361,
"source_countries": 146,
"unique_destinations": 9036,
"unique_source_asns": 3654,
"boomerang_count": "482",
"leaves_country_count": "50450",
"leaves_canada_count": "32088",
"earliest_trace": "2026-04-02T14:24:53.000Z",
"latest_trace": "2026-08-29T09:15:10.000Z"
},
"top_source_countries": [
{ "source_country": "CA", "trace_count": 75260 },
{ "source_country": "US", "trace_count": 24103 }
]
}Counts that come from SQL aggregates are serialised as strings, so parse them as numbers rather than assuming a JSON number type.
leaves_canada_count is a legacy, Canada-specific field kept under its old name for backward compatibility (#363) - it counts CA-sourced traces that left their own source country, not traces that merely touched Canada as the underlying leaves_canada column once did. leaves_country_count is the country-agnostic figure, computed the same way for every country, and is the one to use for anything not specifically about Canada.
EndpointSubmit traceroutes
Submit one or more traceroute results for storage and analysis.
Maximum 50 traces per request. The JSON body limit is 1 MB - split large batches across calls.
Headers
| Name | Required | Description |
|---|---|---|
| X-API-Key | Yes | Your API key |
| Content-Type | Yes | application/json |
Request body
{
"traces": [
{
"host": "example.com",
"dest_ip": "93.184.216.34",
"source_ip": "192.168.1.1",
"source_country": "CA",
"platform": "linux",
"tool": "mtr",
"completed": true,
"hops": [
{
"hop_number": 1,
"ip": "192.168.1.1",
"hostname": "gateway.local",
"rtt_avg": 1.2,
"rtt_min": 0.8,
"rtt_max": 2.1,
"loss_pct": 0
},
{
"hop_number": 2,
"ip": "10.0.0.1",
"rtt_avg": 5.4
}
]
}
]
}Hop fields
| Field | Type | Required | Description |
|---|---|---|---|
| hop_number | integer | Yes | TTL hop position (1-64) |
| ip | string | No | IP address of the hop (null for timeouts) |
| hostname | string | No | Reverse DNS hostname |
| rtt_avg | float | No | Average round-trip time in ms |
| rtt_min | float | No | Minimum RTT in ms |
| rtt_max | float | No | Maximum RTT in ms |
| loss_pct | float | No | Packet loss percentage (0-100) |
Example response
{
"success": true,
"trace_ids": [1042, 1043],
"count": 2
}Error response
{
"success": false,
"error": "Validation failed",
"details": [
{ "trace_index": 0, "errors": ["Invalid dest_ip format"] }
]
}
EndpointASN information
Returns core information about an ASN, including peering quality and route scoring. For observed BGP adjacency (who this ASN is upstream/downstream of), see GET /api/asn/:asn/neighbours below.
Parameters
| Name | Type | Description |
|---|---|---|
| :asn | integer | The AS number to look up (e.g. 577) |
Example request
curl https://bgpscout.io/api/asn/577Example response
{
"asn": 577,
"name": "Bell Canada Backbone",
"country": "CA",
"rir": "ARIN",
"traffic_level": null,
"network_type": "NSP",
"on_ix": true,
"peering_quality": {
"crossing_rate": 6.33,
"quality_tag": "good_peering"
},
"route_score": {
"quality": 5.4,
"route": 7.3,
"overall": 6.4
},
"url": "https://bgpscout.io/asns/577"
}route_score values are on a 0-10 scale. crossing_rate is the percentage of observed traces that left the country. peering_quality, route_score and traffic_level are null when we have not observed enough data for that network - that is a real answer, not an error. An unknown AS number returns 404 with {"error": "ASN not found"}.
EndpointASN adjacency (BGP neighbours)
Every ASN observed adjacent to this one in BGP AS_PATH data, in both directions - what
the source historically used for ASN classification calls its #peers tab, while
bgp.he.net shows on every ASN page. Same shape and
same field names as the MCP tool get_asn_neighbours.
Example request
curl https://bgpscout.io/api/asn/33185/neighboursExample response
Abridged, against the live table on 2026-08-24 (27 neighbours for AS33185). The neighbour arrays are trimmed here; the counts are real.
{
"asn": 33185,
"known": true,
"neighbour_count": 27,
"seen_upstream_ward_count": 5,
"seen_downstream_ward_count": 22,
"returned": 27,
"limit": 100,
"truncated": false,
"seen_upstream_ward": [
{ "asn": 1299, "name": "Arelion (fka. Telia Carrier)", "country": "SE", "observations": 251, "peer_count": 9 },
{ "asn": 3257, "name": "GTT Communications Inc.", "country": "US", "observations": 151, "peer_count": 7 },
{ "asn": 6939, "name": "Hurricane Electric LLC", "country": "US", "observations": 125, "peer_count": 9 },
{ "asn": 174, "name": "Cogent Communications, LLC", "country": "US", "observations": 95, "peer_count": 3 }
],
"seen_downstream_ward": [
{ "asn": 398292, "name": "Babish Telecom", "country": "CA", "observations": 102, "peer_count": 17 }
],
"observed_from": {
"collectors": ["route-views.chicago", "route-views.eqix", "...", "rrc24"],
"collector_count": 12,
"collectors_unreachable_that_run": [],
"built_at": "2026-08-22",
"min_collector_peers_required": 2
},
"notes": {
"relationship": "AS_PATH does not label relationships. An adjacency may be transit OR settlement-free peering OR a sibling, and it is not proof that money changes hands.",
"direction": "\"seen_upstream_ward\" means the neighbour was observed further from the origin in observed paths, and \"seen_downstream_ward\" nearer to it. That is an observation, not proven hierarchy - leaks and siblings break it.",
"coverage": "Observed from 12 BGP route collectors (8 RouteViews + 4 RIPE RIS) on 2026-08-22: route-views.chicago, route-views.eqix, ... rrc24.",
"coverage_limit": "This is a FLOOR, not a peer census, and adding collectors does not lift the ceiling much ..."
}
}Fields
| Field | Description |
|---|---|
| known | false when this ASN has never been observed adjacent to anything - that is different from true with empty arrays, which means we looked and found none within the requested limit (#67: absent is not zero). |
| neighbour_count | The full total for the ASN, read from the precomputed asn_peer_count table - not the number of rows in the arrays below. null when known is false, never 0. Until 2026-08-24 this field was the page size, so a 100-row page of a 1,300-neighbour network reported 200; refetch anything cached from before then. |
| returned, truncated | returned is how many rows the arrays actually hold; truncated is true when that is fewer than neighbour_count. Raise ?limit= (max 500) before treating the arrays as the whole set. |
| observed_from | Which route collectors produced the generation of the table you are being served, when it was built, how many distinct collector peers a pair needed before it was kept, and any configured collector that was unreachable on that run. Read from the build's own provenance record rather than written into the prose, so it cannot go stale. null only if that record is unreadable. |
| observations | How many times this specific adjacency was seen across collected AS_PATHs. |
| peer_count | How many distinct collector-facing peers reported seeing this adjacency - a rough proxy for how well-corroborated it is, independent of raw observation volume. |
The caveats matter more here than on most endpoints, because a longer neighbour list reads as a stronger claim than it is. AS_PATH does not label relationships - an adjacency may be transit, settlement-free peering, or a sibling, not proof money changes hands. Direction is observation, not hierarchy: "upstream-ward" means seen further from the origin, and leaks or sibling ASNs break that assumption. A short or empty result is a statement about collector visibility, not about the network - and that does not go away by adding collectors. An MRT RIB dump carries the best path per prefix per collector peer, so an adjacency that is never the best path anywhere, which is the normal case for settlement-free peering across an exchange route server, is absent from every collector. Measured 2026-08-24 against our own PeeringDB data: of the 2,077 ASNs that are route-server peers at exactly one exchange carrying 200+ other route-server peers, the mean count here is 6.69, and 30% show two or fewer. These are floors.
Data-quality check run 2026-08-21 against the then-live single-collector asn_adjacency table (155,782 rows, 79,484 distinct ASNs, matching asn_peer_count exactly; the twelve-collector build of 2026-08-22 took it to 221,194 rows and 87,177 ASNs): 60 rows (0.04%) name an ASN outside the current RIR-delegated range, almost entirely the 16-bit private-use block (64512-65534) leaking into real AS_PATHs from operator misconfiguration - the same phenomenon #197 documented, not a parsing bug. Clean enough to publish; still worth knowing an undelegated ASN can legitimately appear here.
EndpointEmbeddable ASN widget
Returns a self-contained HTML card for an ASN that can be embedded in an iframe. It shows the ASN name, country, peering-quality badge, route score, and a link back to BGPScout.
Usage
<iframe src="https://bgpscout.io/api/embed/asn/577"
width="320" height="200"
frameborder="0"></iframe>
AlertsAlert delivery webhooks
BGPScout can POST an alert to an HTTPS endpoint you own, so a watch rule reaches your NOC without anyone opening BGPScout. Every request is signed with a per-destination HMAC-SHA256 secret. This section is the receiver's contract: the payload schema, the headers, and the exact verification recipe.
Destinations are managed through the MCP tools list_alert_destinations, create_alert_destination, update_alert_destination, test_alert_destination, delete_alert_destination and get_alert_deliveries. The signing secret is returned once, by the call that creates the destination, and is never readable again. Losing it means rotating it (update_alert_destination with rotate_secret), which is a deliberate one-way door.
Requirements for your endpoint
| What | Value |
|---|---|
| Scheme and port | https:// on port 443 only. Plain HTTP, a non-443 port, and URLs carrying embedded credentials are refused when the destination is saved and again on every single delivery. |
| Address | Must resolve to a public address. Loopback, RFC1918, carrier-grade NAT, link-local, unique-local, multicast, documentation and cloud-metadata ranges are rejected for both IPv4 and IPv6, including IPv4-mapped and NAT64-wrapped forms. The name is re-resolved on every attempt and the connection is made to the address that was validated, so a DNS answer that changes after validation cannot be used. |
| Redirects | Never followed. A 3xx is recorded as a failed delivery (redirect_rejected). To move your endpoint, update the destination URL. |
| Response | Any 2xx means delivered. Reply quickly and do the work asynchronously: the request times out after 10 seconds, and at most 64 KB of your response body is read. |
| Retries | A 5xx or 429 is retried with exponential backoff plus jitter (1 minute doubling to a 30 minute cap). Any other 4xx is also retried. After 5 attempts the delivery stops permanently and is marked dead, visible to the destination owner through get_alert_deliveries. |
Payload
The body is JSON, version-stamped by schema_version. Fields are added compatibly within a version; a receiver should ignore fields it does not recognise.
{
"schema_version": 1,
"event_id": "watchlist_alert:13112",
"event_type": "ixp_joined",
"occurred_at": "2026-08-26T02:14:07.000Z",
"severity": null,
"subject": { "type": "asn", "id": "64500" },
"summary": "AS64500 joined DE-CIX Frankfurt (seen 2026-08-25) [rule: my rule]",
"facts": {},
"evidence": [],
"links": { "reference": "/asns/64500" },
"test": false
}| Field | Meaning |
|---|---|
| schema_version | Integer. 1 today. A receiver should reject a major version it does not understand rather than guess. |
| event_id | Stable and unique per alert: watchlist_alert:<id> for a real alert, test:<random> for a synthetic test. Delivery is at-least-once by design, so dedupe on this value. |
| event_type | The alert type, for example ixp_joined, ixp_left, facility_added, asn_registered. test.synthetic_event for a test send. |
| occurred_at | ISO 8601 UTC. When the alert was created, not when it was delivered. |
| severity | Currently null on every real alert. BGPScout does not yet record a severity on an alert, and a guessed value would be worse than an honest one. Test sends carry "info". Do not build a filter that requires this field to be non-null. |
| subject | What the alert is about: type is one of asn, ixp, facility, org, prefix, cdn, and id is the identifier within that type. |
| summary | Human-readable one-line description, including which rule fired. |
| facts / evidence | Reserved for structured detail. Present and empty today; they will not change type. |
| links | reference is a path on bgpscout.io for the subject, when one can be named honestly. An empty object means there is no page to link, not an error. |
| test | true only for test_alert_destination. A test send never creates a real alert and never touches the delivery queue. Treat a payload with test: true as a connectivity check, never as an incident. |
Headers
| Header | Value |
|---|---|
| BGPScout-Event-Id | Same value as event_id in the body. The idempotency key. |
| BGPScout-Event-Type | Same value as event_type in the body. |
| BGPScout-Timestamp | Unix seconds at the moment of signing. Part of what is signed. |
| BGPScout-Signature | v1= followed by the hex HMAC-SHA256. The v1= prefix is the signature scheme version, not the payload version. |
| Content-Type | application/json |
| User-Agent | BGPScout-Webhooks/1.0 |
Verifying the signature
- Read the
BGPScout-Timestampheader and the raw request body exactly as received. Do not re-serialise the parsed JSON: key order and whitespace are part of what was signed. - Reject the request if
abs(now - timestamp)exceeds your replay window. We recommend 300 seconds. This is what makes a captured request unreplayable, because the signature covers the timestamp. - Compute
HMAC-SHA256(secret, timestamp + "." + rawBody)and hex-encode it. - Compare it against the value after
v1=using a constant-time comparison. Never use a plain string equality on a secret-derived value. - During a secret rotation the sender may sign with either the current or the previous secret for a 24 hour overlap. A receiver holding both should accept either.
// Node.js reference implementation. Note express.raw:
// a JSON body parser would destroy the exact bytes that were signed.
const crypto = require('crypto');
app.post('/bgpscout-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('BGPScout-Timestamp');
const sig = req.get('BGPScout-Signature') || '';
const raw = req.body; // a Buffer, unparsed
if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) {
return res.status(400).send('stale timestamp');
}
const expected = crypto.createHmac('sha256', YOUR_SECRET)
.update(ts + '.' + raw.toString('utf8'))
.digest('hex');
const given = Buffer.from(sig.replace(/^v1=/, ''), 'hex');
const want = Buffer.from(expected, 'hex');
if (given.length !== want.length || !crypto.timingSafeEqual(given, want)) {
return res.status(401).send('bad signature');
}
// Dedupe on the event id: delivery is at-least-once.
if (alreadyHandled(req.get('BGPScout-Event-Id'))) return res.status(200).end();
res.status(200).end(); // acknowledge FIRST, then do the work
handleEvent(JSON.parse(raw));
});A destination is not armed by the act of saving a URL. It is created disabled, and it can only be enabled after test_alert_destination has produced a real 2xx from your endpoint. Changing the URL clears that verification and disables the destination again.
ToolsMTR client
The BGPScout MTR client collects traceroute data and submits it to the API automatically.
Download
Installation
# Linux / macOS
chmod +x bgpscout-mtr
sudo mv bgpscout-mtr /usr/local/bin/
# Verify installation
bgpscout-mtr --versionConfiguration
# Set your API key (from /account)
bgpscout-mtr config --api-key YOUR_API_KEY
# Optionally set your source ASN
bgpscout-mtr config --source-asn 64500Usage
# Run a single trace
bgpscout-mtr trace example.com
# Run against Tranco top sites (batch mode)
bgpscout-mtr batch --targets tranco-top100.txt
# Run with automatic submission
bgpscout-mtr trace --submit example.com
# Schedule recurring traces (cron-friendly)
bgpscout-mtr batch --targets targets.txt --submit --quietExample output
$ bgpscout-mtr trace --submit google.ca
Tracing google.ca (142.250.80.67)...
1 gateway.local (192.168.1.1) 1.2ms
2 10.0.0.1 5.4ms
3 isp-core.example.net 12.1ms
...
Submitted trace ID: 1042