GET /api/query/stats
Graph-wide node and edge counts, threat-intel summary, and per-layer freshness and coverage. No key needed. Code in five languages.
GET /api/query/stats Documentation
GET /api/query/stats returns graph-wide counts, a threat-intel summary, and the state of every computed layer. It is the cheapest way to learn the shape of the graph, and the place to check that the layer a result depends on is healthy before you trust the result.
Use it instead of a global Cypher count. A query like MATCH ()-[r]->() RETURN count(r) counts edges over unanchored endpoints, and the engine refuses it: HTTP 400, query-unservable with reason: "global_edge_count", and this endpoint named in the suggestions array. For per-edge-type counts, use CALL db.relationshipTypes() YIELD type, count; this endpoint returns totals, not per-type counts.
No key is needed: the body is the same signed in or out. The response carries Cache-Control: max-age=60; honor it and reuse the last body for that long rather than polling.
Base URL: https://graph.whisper.security. No request body and no parameters.
Call it
curl -s -A "whisper-client/1.0" \
"https://graph.whisper.security/api/query/stats"Response
Captured 2026-09-02 and abridged: one entry is shown under vdp.layers, and rebuild is left out. The numbers move, so read them from the live response rather than from this page.
{
"physical": {"nodeCount": 3800146986, "edgeCount": 31526259743},
"virtual": {"nodeCount": 3682439526, "edgeCount": 8023477096},
"total": {"nodeCount": 7482586512, "edgeCount": 39549736839},
"objectCount": 47032323351,
"threatIntel": {
"threatIntelLoaded": true,
"hasTaxonomy": true,
"available": true,
"feedSourceCount": 134,
"categoryCount": 32,
"totalListedInEdges": 10239820,
"asnEnrichmentLoaded": true,
"prefixBgpEnrichmentLoaded": true,
"prefixBgpEnrichmentMatchesLive": true,
"prefixBgpEnrichmentUsable": true,
"dominantCityAnswerable": true
},
"vdp": {
"total_layers": 20,
"ready_layers": 20,
"degraded_layers": 1,
"virtual_nodes_by_layer": {"dns-resolves": 0},
"virtual_edges_by_layer": {"dns-resolves": {"RESOLVES_TO": 1174924}},
"layers": [
{
"name": "dns-resolves",
"ready": true,
"claimed_labels": [],
"claimed_edge_types": [],
"node_count": 0,
"edge_count": 1174924,
"edge_count_by_type": {"RESOLVES_TO": 1174924},
"last_refresh_epoch_millis": 1788362774048,
"refresh_in_progress": false,
"coverage": "OK",
"coverage_by_edge_type": {"RESOLVES_TO": "OK"}
}
]
},
"timestamp": "2026-09-02T16:01:47Z"
}
| Field | What it holds |
|---|---|
physical | Nodes and edges stored on disk. |
virtual | Objects computed at query time from live routing, DNS and threat-intelligence data, such as ANNOUNCED_BY and LISTED_IN edges. |
total | The sum of physical and virtual. |
objectCount | All nodes and edges added together. |
threatIntel | Whether the threat-intel layer and its taxonomy are loaded (threatIntelLoaded, hasTaxonomy, available), the size of the feed catalogue (feedSourceCount, categoryCount; 134 feeds in 32 categories as of the last census), the number of LISTED_IN edges (totalListedInEdges), and the enrichment flags described below. |
vdp | The computed layers: how many exist (total_layers), how many are ready or degraded, per-layer object counts (virtual_nodes_by_layer, virtual_edges_by_layer), and one entry per layer under layers. |
rebuild | Whether a background rebuild of the enrichment indexes is running (rebuildInProgress) and their readiness flags. |
timestamp | When the snapshot was taken (UTC). |
Read the enrichment flags
Four booleans in threatIntel say whether an answer that depends on enrichment is being served from current data. asnEnrichmentLoaded and the three prefixBgpEnrichment* flags cover network attribution (IP to announced prefix to ASN); dominantCityAnswerable covers city-level geolocation. When one of them is false, hold the corresponding conclusion until it returns to true.
Read a layer before you trust a result
Every entry in vdp.layers describes one computed layer:
| Field | What it holds |
|---|---|
name | The layer's name. |
ready | Whether the layer is loaded and serving. |
claimed_labels, claimed_edge_types | The node labels and edge types this layer supplies. Match them against the labels and edges in your query. |
node_count, edge_count, edge_count_by_type | What the layer holds right now. |
last_refresh_epoch_millis | When the layer last refreshed, as a Unix time in milliseconds. |
refresh_in_progress | Whether a refresh is running. |
coverage | OK, DEGRADED or EMPTY. coverage_by_edge_type gives the same value per edge type. |
Before you rely on a result, find the layer that supplies the edge type or label you traversed and check two things: coverage is OK, and last_refresh_epoch_millis is as recent as your use case needs. A DEGRADED layer answers, but with less than it normally holds, so a thin or empty result drawn from it is not evidence of absence. An EMPTY layer holds nothing, and a query through it returns no rows. A vdp.degraded_layers value above zero is the quick signal that one layer needs this check, and a response that carries a vdp-plane-empty or vdp-anchor-empty advisory is telling you the same thing about the query you just ran; see Advisories.
For running actual queries, see POST /api/query.