# Procedure Reference

> Every WhisperGraph stored procedure with its arguments and its exact YIELD columns: how CALL binds them, the verdict, history, discovery, CVE, routing, suffix and export families, the advisories channel, and the whole registered surface.

*Source: https://www.whisper.security/docs/whisper-graph/procedures/reference*

---
The complete calling contract for every stored procedure the engine registers. Each entry gives the argument the procedure takes and the `YIELD` columns it emits, in the order the engine emits them, so you can write the `YIELD` clause without guessing. Seven procedures have a page of their own with worked examples; they are listed on [Procedures](/docs/whisper-graph/procedures). Several also have a matching tool on the [MCP server](/docs/ai/mcp/reference).

## Calling a procedure

`CALL` runs a procedure in three forms: standalone, with `YIELD` to pick and filter columns, or once per row when the argument comes from `UNWIND`, `WITH` or `MATCH`. A standalone call with no `YIELD` returns every column the procedure produces. Full clause syntax is in [Syntax & Clauses](/docs/cypher/syntax).

```cypher expect=rows>0 seed=paypal.com verified=2026-09-02
CALL whisper.variants("paypal.com")
YIELD variant, method, exists, confidenceLabel
WHERE exists
RETURN variant, method, confidenceLabel
LIMIT 6
```

```cypher expect=rows>0 seed=google.com verified=2026-09-02
UNWIND ["google.com", "cloudflare.com"] AS d
CALL whisper.psl.tldPlusOne(d) YIELD apex
RETURN d, apex
```

Five rules cover nearly every failed call:

- **Quote every argument.** `CALL whisper.identify(ubuntu.com)` is rejected as a bad argument, and an unquoted IPv6 literal is parsed as something else entirely and comes back as a syntax error. Always `CALL whisper.identify("ubuntu.com")`.
- **Argument types matter as much as names.** `whisper.topAsnsByPrefixCount(10)` takes an Integer; `whisper.explain.bundle` takes one string and rejects a list; `whisper.export` takes exactly one map.
- **`YIELD` columns are exact contracts.** A column the procedure does not emit is rejected, not ignored, and the message names the valid columns. `db.relationshipTypes()` yields `type`, not `relationshipType`.
- **Multi-shape procedures need named columns.** `explain` and `whisper.history` change their column set with the indicator, so `YIELD *` is rejected, and so is a `YIELD` that mixes shapes. Name columns from one shape, or call the single-shape variant: `whisper.explain.bundle`, `whisper.history.whois`, `whisper.history.bgp`.
- **A URL folds to its host.** `whisper.identify`, `whisper.assess`, `whisper.walk` and the history procedures read `https://host/path?q=1` as `host`. `whisper.assess`, `whisper.assessUrl`, `whisper.identify` and `whisper.enrich` take a single string or a list.

## The procedures

Every procedure the engine registers, grouped by what it answers. The `YIELD` columns are exact contracts: a column the procedure does not emit is rejected, not ignored.

> **Read `coverage` before `band`.** Only `known-clean` licenses the word "clean"; `no-data` means
> *unknown*, which is a different thing again; `malicious-evidenced` and `ambiguous` mean there is
> evidence, whatever the band says. `whisper.explain` does not return `coverage` at all.
> Full contract: [Coverage — what we looked at](/docs/whisper-graph/procedures/coverage).

### Threat & verdict

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `explain(indicator)`, also `whisper.explain` | one string: IP, hostname, ASN, CIDR, file hash or CVE id | `indicator, type, available, cached, found, score, level, explanation, factors, sources, breakdown, advisory, verdictScore` | Multi-shape; name the columns. `sources[]` entries carry `feedId, weight, firstSeen, lastSeen` |
| `whisper.explain.bundle(indicator)` | one string, never a list | `verdict` | One map column that never shifts with indicator type; reach in with `verdict.level`, `verdict.score`, `verdict.found`, `verdict.explanation` |
| [`whisper.assess(hosts)`](/docs/whisper-graph/procedures/identify#whisper-assess-hosts-is-it-dangerous) | a list or a single string; a URL folds to its host | `host, label, band, sub_labels, signals, coverage, evidence, verdictScore` | Read `coverage` before `band` |
| `whisper.assessUrl(urls)` | a list or a single string | `url, host, path, apex_band, path_band, band, coverage, evidence` | `coverage` describes the path, not the host |
| `whisper.enrich(names)` | a list or a single string | `name, owner, country, asn, band, prevalence, coverage` | Rows are de-duplicated by canonical name, so join back by `name`, never by position. `owner` is the network operator of the resolved IP's origin AS: a network attribution, never a threat attribution. `prevalence` is a popularity rank where lower is more prevalent and `null` is unranked |

### History

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `whisper.history(indicator)` | one string | WHOIS columns for a domain; routing columns for an IP, ASN or prefix | Multi-shape: `YIELD` within one shape, or call a variant below |
| `whisper.history.whois(domain)` | one string; a URL folds to its host and a subdomain to its registrable apex | `indicator, type, queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers, cached, registrableDomain` | A fold adds a `whois-parent-fold` advisory to the response |
| `whisper.history.bgp(indicator)` | one string: an IP, ASN or prefix | `indicator, type, origin, prefix, startTime, endTime, visibility, peersSeing, cached` | Note the spelling of `peersSeing` |

### Attribution & discovery

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `whisper.identify(hosts)` | a list or a single string; a URL folds to its host | `host, vendor_id, canonical_name, is_canonical, confidence, category, roles, band, host_class, evidence` | Who runs the host, not whether it is legitimate. Large batches are rejected, not truncated |
| [`whisper.walk(host[, depth, budget])`](/docs/whisper-graph/procedures/identify#whisper-walk-host-depth-budget-ms-the-structural-neighborhood) | a string, then optional Integers | `host, no_atlas_match, nearest_known_vendors, siblings, coverage, arms` | `coverage` is presence, not a verdict: `structural-only`, `no-data` or `deadline-hit` |
| `whisper.origins(domain[, options])` | a string, optional map `{include_related: true}` | `ip, confidence, methods, asn, asnName, kind, category, truncated` | `confidence` runs 0.0 to 1.0; passive, nothing touches the target |
| `whisper.resolve(host)` | one string | `host, a, aaaa, freshest_observation_ms, coverage` | Current A and AAAA records from passive data |
| [`whisper.search(token[, options])`](/docs/whisper-graph/procedures/helpers#whisper-search-token-options) | a string, optional map with `types`, `mode`, `suffix`, `limit`, `timeoutMs` | `query, kind, name, matchedField, matchType, warning, score` | The bounded front door for a token you cannot classify |
| `whisper.audit.malformedHostnames(zone)` | one string | `clean, malformed, total, samples, scope, truncated` | Splits a zone's children into clean and malformed names |
| `whisper.variants(domain)` | a string; optional node label or `false` as the filter | `variant, method, exists, confidence, confidenceLabel` | `exists: true` means registered, not malicious. Also callable as a function in expression position |
| `whisper.lookupTlsFingerprint(hash)` | a string: a bare hash or `kind:hash` | `indicator, found, kind, hash, category, label, family, vendor, client, trustTier, sourceCount, firstSeen, lastSeen, licensePosture` | `found: false` is a populated row |
| `whisper.lookupTorRelay(ip)` | a string: an exit IP or a relay fingerprint | `indicator, found, fingerprint, exitAddresses, exitAddressCount, exitAddressesV6, exitAddressCountV6, source, ingestedAt` | `found: false` is a populated row |
| `whisper.danglingCname(hosts)` | a string or a list | `host, target, target_apex, target_apex_state, observed_at` | Zero rows on a clean host; `target_apex_state: UNREGISTERED` is the takeover signal |

### CVE plane

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| [`whisper.cve.byPackage(cpe)`](/docs/whisper-graph/procedures/helpers#cve-plane) | one full CPE 2.3 string | `cve, band, kev, ransomware, epss, cvss, coverage` | Always at least one row. A spec it cannot read, such as a Package URL, returns one row with `coverage: "unsupported-spec"` and everything else null |
| [`whisper.vulnPosture(target)`](/docs/whisper-graph/procedures/helpers#cve-plane) | a hostname, an ASN, or a map with `cves`, `packages` or `cpes` | `openCveCount, scoredCount, critical, high, medium, low, kevCount, ransomwareCount, maxEpss, maxCvss, priority, coverage` | Always exactly one row; read `coverage` before the counts |

### Infrastructure & BGP

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `whisper.asnThreatDensity(asn)` | one string, `"AS13335"` | `asn, listedIps, announcedIpv4, densityRatio, routedPrefixes, coverage` | Listed addresses against announced space |
| `whisper.asnCountries(n)` | Integer | `country, asns` | ASN count per registration country |
| `whisper.topAsnsByPrefixCount(n)` | Integer | `asn, prefixCount` | The networks announcing the most prefixes |
| `whisper.bgpDegreeDistribution()` | none | `inDegree, outDegree, asnCount` | A histogram: one row per degree pair |
| `whisper.asSet(name)` | one string, an IRR as-set name | `asSetName, memberAsn, sourceRir` | Membership of an IRR as-set, one row per member ASN |

### Public Suffix List

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `whisper.psl.tldPlusOne(host)` | one string | `apex` | The registrable apex (eTLD+1) |
| `whisper.psl.isPublicSuffix(name)` | one string | `result` | A single boolean |
| `whisper.psl.affiliation(host)` | exactly one string | `found, suffix, submitterLogin, submitterOrg, evidenceKind, confidence` | `found: false` is a populated row |

### Threat-intel snapshot candidates

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `whisper.threatIntel.candidateCdnApex(n)` | Integer | `apex, subCount, certCount, wildcardCount, isOnPslPrivate, recommendation, computedAt` | Precomputed CDN and multi-tenant apex candidates |
| `whisper.threatIntel.candidateMultiTenantApex(n)` | Integer | `name, nodeId, subCount, threatSources, threatScore, isOnDenyList, recommendation, computedAt` | Zero rows means the snapshot holds no candidates of this class |
| `whisper.threatIntel.candidateSharedHostingIp(n)` | Integer | `ip, nodeId, hostCount, threatSources, threatScore, isAlreadyMarked, recommendation, computedAt` | Same |

### Bulk export

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| [`whisper.export(options)`](/docs/whisper-graph/procedures/helpers#bulk-export) | exactly one map `{label, limit, cursor}`; `label` is required and is `malicious`, `ambiguous` or `benign-allowlisted` | `host, label, ip, cidr, asn, url_paths, cert_shas, tls_fingerprints, dns, last_seen, coverage, truncated, supersedes, look_alike_negatives, next_cursor` | Always pass `limit`; page by feeding a row's opaque `next_cursor` back as `cursor` |

### Your own context

| Procedure | Argument | `YIELD` columns | Read it like this |
|-----------|----------|-----------------|-------------------|
| `whisper.quota()` | none | `key, value` | One row per key describing your own service context: who the server takes you for and whether it recognised your key. Ask the key, never a page |

## Every procedure

The tables above are the ones worth learning first. This one is the whole surface, generated from `CALL db.procedures()` — every procedure the engine registers, whether or not a page has been written about it. **Mode** is the engine's own, and it describes the ENGINE, not this API: `READ` answers a question; `WRITE` names a procedure that would change something, and that you cannot call from here. The public Cypher endpoint rejects mutating calls, and the MCP server exposes read-only tools only — it has no contribution or feedback tool. There is no route by which a reader of this page writes to the graph.

| Procedure | Mode | What it does |
|-----------|------|--------------|
| `db.functions` | READ | List all available Cypher functions (this procedure) |
| `db.labels` | READ | List all node labels with row counts |
| `db.procedures` | READ | List all registered procedures (this procedure) |
| `db.propertyKeys` | READ | List all property keys |
| `db.relationshipTypes` | READ | List all relationship types with source/target labels |
| `db.schema` | READ | Full schema description (labels + types + counts) |
| `db.schema.nodeTypeProperties` | READ | Per-node-label property index |
| `db.schema.relTypeProperties` | READ | Per-relationship-type property index |
| `db.schema.visualization` | READ | Schema graph for visualization |
| `dbms.components` | READ | Server component listing (Neo4j-driver compat) — one row {name='whisper-ng', versions=[<ver>], edition='community'}. |
| `explain` | READ | Threat-assessment explanation for an indicator (IP, hostname, ASN, CIDR). level enumerates {NONE, INFO, LOW, MEDIUM, HIGH, CRITICAL}… |
| `whisper.asSet` | READ | IRR as-set MEMBERSHIP lookup (NOT asset management) — one row per member ASN of the named as-set, served from the local IRR snapshot; no upstream call. |
| `whisper.asnCountries` | READ | ASN count per country ((:ASN)-[:HAS_COUNTRY]->(:COUNTRY)); one row {country, asns} ordered by count DESC… |
| `whisper.asnThreatDensity` | READ | Per-ASN threat density — one row {asn, listedIps, announcedIpv4, densityRatio, routedPrefixes… |
| `whisper.assess` | READ | Maliciousness-verdict surface for a list of hosts — one row per host with {host, label, band, sub_labels[], signals[], coverage, evidence[], verdictScore, isThreat… |
| `whisper.assessUrl` | READ | URL-scoped maliciousness-verdict surface for a list of URLs — one row per URL {url, host, path, apex_band, path_band, band, coverage… |
| `whisper.audit.malformedHostnames` | READ | Per-zone HostnameValidator audit — partitions a bounded CHILD_OF scan into clean/malformed buckets. |
| `whisper.bgpDegreeDistribution` | READ | Global BGP AS-adjacency degree DISTRIBUTION — one row per (in,out) degree bucket {inDegree, outDegree, asnCount} (all Long)… |
| `whisper.cve.byPackage` | READ | Affecting-CVE listing for ONE package/cpe — whisper.cve.byPackage(spec) where spec is a cpe:2.3 string, {cpe:'cpe:2.3:...'}, or {name, os, osVersion} (distro key). |
| `whisper.danglingCname` | READ | Host-anchored dangling-CNAME feed lookup — whisper.danglingCname(host | host[]). |
| `whisper.enrich` | READ | Batched endpoint enrichment for a list of names — whisper.enrich(name[]) returns ONE order-preserving row per canonical name {name, owner, country, asn, band, prevalence, coverage}… |
| `whisper.explain` | READ | Alias for explain |
| `whisper.explain.bundle` | READ | Threat-assessment as a single {verdict: Map} column (single-shape variant of explain) |
| `whisper.export` | READ | Read-only bulk export of the threat corpus by label (`malicious`, `ambiguous`, `benign-allowlisted`), for classifier distillation. |
| `whisper.history` | READ | Historical WHOIS / BGP data for an indicator (auto-pivot) |
| `whisper.history.bgp` | READ | BGP routing history for IP / ASN / prefix (type-strict, single-shape) |
| `whisper.history.whois` | READ | Domain WHOIS history (type-strict, single-shape) |
| `whisper.identify` | READ | Host-first vendor attribution over the GOLD RESOLVES_TO->IPV4->DELEGATED_TO->VENDOR path, with an ORIGIN_AS org-graph long tail. |
| `whisper.lookupTlsFingerprint` | READ | TLS handshake fingerprint lookup — probes all 8 kinds (ja3/ja4/ja4s/ja4h/ja4x/ja4t/ja4tscan/jarm) or accepts a kind:hash composite. |
| `whisper.lookupTorRelay` | READ | Tor exit-relay lookup — dual-input (a 40-hex Ed25519 fingerprint OR a single exit IPv4/IPv6 address). |
| `whisper.origins` | READ | Discover candidate origin IPs behind a CDN, scored by independent evidence |
| `whisper.psl.affiliation` | READ | PSL submitter-affiliation lookup by private suffix or hostname. |
| `whisper.psl.isPublicSuffix` | READ | True if the input matches a Public Suffix List entry. |
| `whisper.psl.tldPlusOne` | READ | Registrable apex (eTLD+1) lookup via the Public Suffix List. |
| `whisper.quota` | READ | Where the calling key stands right now. Ask the key, never a page. |
| `whisper.resolve` | READ | Read-only DNS resolution for a single host — whisper.resolve(host). |
| `whisper.search` | READ | Bounded analyst search — routes an untyped token to an exact index lookup (IPv4/IPv6/CIDR/ASN/hostname/exact ASN-name), a bounded FST prefix scan… |
| `whisper.submit` | WRITE | Contribute an observation back — an indicator or a corroboration receipt. It is a write, so it needs a signed-in key. |
| `whisper.threatIntel.candidateCdnApex` | READ | Top-K precomputed CDN / multi-tenant CA apex candidates from CT + PSL grouping. |
| `whisper.threatIntel.candidateMultiTenantApex` | READ | Top-K precomputed multi-tenant apex candidates from the threat-intel snapshot. |
| `whisper.threatIntel.candidateSharedHostingIp` | READ | Top-K precomputed shared-hosting IPV4 candidates from the threat-intel snapshot. |
| `whisper.topAsnsByPrefixCount` | READ | Top-N ASNs ordered by announced-prefix count; served O(1) from a precomputed snapshot refreshed at BGP cadence. |
| `whisper.variants` | READ | Lookup variants of a hostname/domain |
| `whisper.version` | READ | Server version + build time — one row {version, buildTime}. |
| `whisper.vulnPosture` | READ | SBOM/CVE-set posture — whisper.vulnPosture({cves:[...], packages:[{name,version,ecosystem}], cpes:[...], os, osVersion}). |
| `whisper.walk` | READ | Structural-neighborhood fallback for a NOVEL host (whisper.walk(host[, depth[, budget_ms]])). |
| `whisper.watch` | WRITE | Create, list and cancel subscriptions to a query, a verdict or an indicator. |

> **The `WRITE` rows are engine capabilities, not something you can call here.** `whisper.submit` and `whisper.watch` are registered by the engine and reported by `db.procedures()`, which is why they appear in this table. **They are not reachable through the MCP server**, which exposes read-only tools and refuses write procedures by name, and not through the public Cypher endpoint, which rejects any mutating call. There is no contribution or feedback tool on the MCP surface. Treat this column as a statement about the engine, not an invitation.

*Rows generated from `CALL db.procedures() YIELD name, signature, description, mode RETURN name, signature, description, mode ORDER BY name` against https://graph.whisper.security, fetched 2026-09-20T01:36:24Z.*

## Read the advisories channel

A successful response can carry a top-level `advisories[]` array beside `columns`, `rows` and `statistics`. Each entry has a `kind`, a human `message` and, where they apply, the `queried` input and the `resolved` value. It lives on the response envelope, not in a row, so it survives any `YIELD` or `RETURN` projection, and the key is omitted when there is nothing to say: test for its presence rather than expecting an empty array. The kinds you will meet on this surface:

| `kind` | Emitted by | What to do |
|--------|------------|------------|
| `enrich-semantics` | `whisper.enrich` | Read it once. It restates how `owner`, `prevalence` and row de-duplication work |
| `whois-parent-fold` | `whisper.history.whois` | `queried` was folded to `resolved`; the WHOIS shown belongs to the registrable parent |
| `explain-verdict-axis-unavailable` | `explain` on an ASN | `score` and `level` are placeholders on that row. Read `breakdown.reputationScore` and `breakdown.reputationCategory`, and do not compare them with a threat band |
| `explain-score-unavailable` | `explain` | The `score` column holds no usable value for that row. Read `level`, `explanation` and `factors[]` |
| `origins-all-candidates-withheld` | `whisper.origins` | Every candidate was contextual CDN or shared-provider infrastructure. Re-run with `{include_related: true}` to see them, labelled with the reason |
| `projection-verdict-omitted` | a query that projects a node without its verdict fields | If the verdict is what you need, project `verdictLevel` and `verdictCoverage`, or call `whisper.assess` |

## When to prefer a procedure over a traversal

Reach for the procedures first. They answer the hardest questions in one call, usually faster and cleaner than a hand-written deep traversal.

- **The logic runs server-side.** `explain()` computes a score from feed count, feed weights, recency and the age of the listings, and hands back the arithmetic in `factors[]` with the named feeds in `sources[]`. Reproducing that by walking `LISTED_IN` edges yourself takes more hops and gives you less evidence.
- **A procedure replaces a slow scan.** Where a hand-written query walks the graph itself, a procedure does the same work in one call, which makes it the standard fix for a query that runs long — alongside anchoring the query and adding a `LIMIT`. See [Best Practices](/docs/cypher/best-practices).
- **The output is decision-ready.** A procedure returns labeled columns you can paste straight into a ticket.

One caveat: BGP routing history over a large network is slow. Keep a `LIMIT` on `whisper.history.bgp()` calls and expect a longer round trip.

## Access

Some of these calls need an API key. Pass it in the `X-API-Key` header; [sign in](https://console.whisper.security/sign-in?redirect_url=https%3A%2F%2Fwww.whisper.security%2Fdocs%2Fwhisper-graph%2Fprocedures) to get one — there is no card to enter.

## Schema introspection {#schema-introspection}

The `db.*` procedures describe the live schema, so you can confirm a label or edge exists before you anchor on it.

```cypher expect=rows>0 verified=2026-09-02
CALL db.labels() YIELD label RETURN label ORDER BY label LIMIT 12
```

`db.labels()` lists every node label with its count. `db.relationshipTypes()` lists every edge type as `type` (not `relationshipType`) with its source and target labels, and flags an edge that is declared but currently empty. `db.propertyKeys()` lists every property name in use. `db.schema()` returns a structured overview of the whole graph and accepts a format argument (`"json"`, `"markdown"`, or `"details"`), which collapses the result into a single `schema` column; `db.schema.nodeTypeProperties()` and `db.schema.relTypeProperties()` list the properties on each label and edge type. `db.functions()` and `db.procedures()` list the callable surface itself. The full label, edge, and property model is on the [Graph Schema](/docs/whisper-graph/schema) pages.

| Procedure | Argument | `YIELD` columns |
|-----------|----------|-----------------|
| `db.labels()` | none | `label, count` |
| `db.relationshipTypes()` | none | `type, count, sourceLabels, targetLabels, aliasOf, declaredButEmpty, sparseSourceLabels` |
| `db.propertyKeys()` | none | `propertyKey` |
| `db.schema()` | none, or one of `"json"`, `"markdown"`, `"details"` | without an argument `type, name, count, description, example, sourceLabels, targetLabels, fastPatterns, slowPatterns, bestPractices`; with a format, a single `schema` column |
| `db.schema.nodeTypeProperties()` | none | `nodeType, nodeLabels, propertyName, propertyTypes, mandatory` |
| `db.schema.relTypeProperties()` | none | `relType, propertyName, propertyTypes, mandatory` |
| `db.schema.visualization()` | none | `schema` |
| `db.functions()` | none | `name, signature, description, category` |
| `db.procedures()` | none | `name, signature, description, mode` |
| `dbms.components()` | none | `name, versions, edition` |
| `whisper.version()` | none | `version, buildTime` |
