Skip to content
HTTP API
Skip navigation
HTTP API
View as Markdown

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.

Published Last updated

On this page (4)

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.

json
{
  "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"
}
FieldWhat it holds
physicalNodes and edges stored on disk.
virtualObjects computed at query time from live routing, DNS and threat-intelligence data, such as ANNOUNCED_BY and LISTED_IN edges.
totalThe sum of physical and virtual.
objectCountAll nodes and edges added together.
threatIntelWhether 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.
vdpThe 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.
rebuildWhether a background rebuild of the enrichment indexes is running (rebuildInProgress) and their readiness flags.
timestampWhen 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:

FieldWhat it holds
nameThe layer's name.
readyWhether the layer is loaded and serving.
claimed_labels, claimed_edge_typesThe 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_typeWhat the layer holds right now.
last_refresh_epoch_millisWhen the layer last refreshed, as a Unix time in milliseconds.
refresh_in_progressWhether a refresh is running.
coverageOK, 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.