# Cheat Sheet

> One-page Cypher cheat sheet for WhisperGraph: labels, edge directions, traversal chains, procedure one-liners with exact YIELD columns, and query rules.

*Source: https://www.whisper.security/docs/cypher/cheat-sheet*

---
WhisperGraph is the internet as a queryable database — DNS, BGP/RPKI, WHOIS/RDAP, GeoIP, email posture, certificate transparency, threat intel, and the physical internet, all pre-joined. This page is the dense quick-reference: labels, edges with directions, the traversal chains worth memorizing, and procedure one-liners with their exact columns. If you already know Cypher, this is the WhisperGraph-specific part you need. Deeper detail lives in the [Graph Schema](/docs/whisper-graph/schema), [Procedures](/docs/whisper-graph/procedures), and the [Workflows](/docs/workflows).

**Scale:** 7.5B nodes · 39.8B edges · 41 labels · 53 edge types · 52 procedures · 134 intelligence feeds (112 of them threat feeds) · 32 categories. `GET /api/query/stats` returns live totals.

---

## Endpoint

```bash
POST https://graph.whisper.security/api/query
Content-Type: application/json
User-Agent: whisper-client/1.0   # send an explicit UA to avoid a WAF 403
X-API-Key: <your-key>            # sign in to get one — see /docs/whisper-graph/getting-started

{"query": "MATCH (h:HOSTNAME {name:\"www.google.com\"})-[:RESOLVES_TO]->(ip) RETURN ip.name LIMIT 3"}
```

Bind `$name` placeholders through a sibling `"parameters": {...}` object; `;`-separated statements run as a batch and come back as a `results` array. MCP for agents at `https://mcp.whisper.security`. See [MCP setup](/docs/ai/mcp/setup). Full API details: [HTTP API](/docs/cypher-api).

---

## Node labels

There is **no `Domain` or `FQDN` label** — every name is a `HOSTNAME`, and a legacy label (`Domain`, `IpAddress`, `Certificate`) is rejected with an error naming the replacement. Every node has a `name` property except `ROA`. Names are lowercase with no trailing dot. Labels marked *(computed)* are synthesized at query time — reach them through an edge from an anchored node, never via an unanchored scan.

**Core DNS & addressing** — `HOSTNAME` (2.8B), `IPV4` (622M), `IPV6`, `PREFIX`, `ANNOUNCED_PREFIX` *(computed)*, `REGISTERED_PREFIX` *(computed; `.rir`, `.country`)*, `TLD`, `URL` *(computed; phishing-kit paths — anchor by `{name}`, which holds the path; `{path}` and `{id}` scan the label)*

**Routing & org** — `ASN` (`AS13335`; the registry is on `.autNumSourceRir`), `ASN_NAME` *(computed)*, `ORGANIZATION` (raw registrant strings; fold with `SAME_ORG_AS`), `TLD_OPERATOR`, `RIR` *(node-only; nothing joins to it)*

**WHOIS & registration** — `REGISTRAR`, `EMAIL`, `PHONE`, `RDAP_ENTITY` (reached from `PREFIX` or `ASN` over `REGISTERED_TO_ENTITY`)

**Geo & DNSSEC** — `CITY`, `COUNTRY`, `DNSSEC_ALGORITHM`

**Threat intel** — `FEED_SOURCE` (134, *computed*; `.name` is the slug, `.displayName` the label), `CATEGORY` (32, *computed*), `THREAT_TAG` (MISP-galaxy, via `TAGGED_AS`), `THREAT_SIGNAL_TYPE` (each signal belongs to one label, for example `prefix-age-anomaly` and `toxic-neighborhood` on `PREFIX`; `bulletproof-hosting`, `critical-infrastructure`, `ddos-mitigation` and `satellite-network` on `ASN`; `hidden-primary-soa`, `subdomain-takeover`, `wildcard-dns` and `infrastructure-staging` on `HOSTNAME`), `ACTOR` (`APT28`, case-sensitive; `.aliases` holds vendor names), `ATTACK_PATTERN` (MITRE ATT&CK; anchor by `{name: "T1003"}`, which the name index resolves by T-number, or filter `{kind: "technique"}`; the T-number reads back as `.id`), `DWI_DOMAIN` *(unjoinable; `.onion` watch entries)*

**RPKI & routing observations** — `ROA` (no `name`; reach it via `ROA_AUTHORIZES_ORIGIN` / `ROA_AUTHORIZES_PREFIX`, read `.prefix`, `.asn`, `.maxLength`; `.authSource` is `rpki-roa` for an RPKI ROA or `irr-route` for a routing-registry route object), `BGP_PATH_OBSERVATION` (`name` is the hyphen-joined AS path, origin last; via `BGP_PATH`)

**Physical infrastructure** — `FACILITY`, `INTERNET_EXCHANGE`, `SUBMARINE_CABLE`, `CABLE_LANDING`, `CDN_POP` (`akamai:peeringdb:164`; group on `.operator`, `.city`, `.countryCode`, not `.name`), `DNS_ROOT_INSTANCE` *(unjoinable)*, `CLOUD_REGION` *(thin)* (`aws:eu-west-1`)

**Egress, fingerprint & transparency** — `VENDOR` (`zoom`, `okta`), `TOR_RELAY` (keyed by fingerprint), `TLS_FINGERPRINT` *(thin)* (`ja3:<hash>`, `jarm:<hash>`), `CT_OBSERVATION` *(thin)* (certificates live here, never on a `Certificate` label), `DMARC_RECIPIENT`

Three markers change how you plan a traversal:

- ***(unjoinable)*** — the nodes exist and list, and **no edge of any type touches them**. `DNS_ROOT_INSTANCE` and `DWI_DOMAIN` are in this state: you can `MATCH` them, you cannot traverse to or from them.
- ***(thin)*** — the plane is real and most seeds will miss it.
- ***(computed)*** — synthesized at query time; anchor the stored end and walk outward.

**A zero-row result on a thin or unjoinable plane means Whisper holds no observation — never that the host has none.**

---

## Edge types — with directions

Directions are **strict**: a wrong-way traversal returns zero rows with no error. The arrow below is the stored direction; traverse backwards with `<-[:EDGE]-`.

### DNS & web

| Edge | From → To | Notes |
|------|-----------|-------|
| `RESOLVES_TO` | HOSTNAME → IPV4/IPV6 | **Forward only.** Reverse DNS: `(ip)<-[:RESOLVES_TO]-(h)`. There is no PTR edge |
| `ALIAS_OF` | HOSTNAME → HOSTNAME | CNAME |
| `CHILD_OF` | HOSTNAME/EMAIL → HOSTNAME/TLD | **child → parent** (var-length walks reach the TLD) |
| `NAMESERVER_FOR` | HOSTNAME → HOSTNAME | **server → domain.** A domain's NS: `(d)<-[:NAMESERVER_FOR]-(ns)` |
| `MAIL_FOR` | HOSTNAME → HOSTNAME | **server → domain.** A domain's MX: `(d)<-[:MAIL_FOR]-(mx)` |
| `LINKS_TO` | URL → HOSTNAME | This phishing-kit path is served by this host. Anchor the URL by `{name}` (the path) or bound it with `WITH u LIMIT n` first. Hostname-to-hostname links are a small sample, not a web layer |

### BGP, routing & RPKI

| Edge | From → To | Notes |
|------|-----------|-------|
| `BELONGS_TO` | IPV4/IPV6 → PREFIX | RIR allocation (also `FEED_SOURCE → CATEGORY`) |
| `ANNOUNCED_BY` | IPV4/IPV6 → ANNOUNCED_PREFIX | *computed*. The IP → prefix step on the way to the origin AS |
| `ROUTES` | ASN → ANNOUNCED_PREFIX/PREFIX | *computed*; matches in either direction |
| `HAS_NAME` | ASN → ASN_NAME | `asn.name` is the AS number; the network name is on `ASN_NAME` (*computed*) |
| `BGP_NEIGHBOR` | ASN ↔ ASN | Peering adjacency. **Use this, not `PEERS_WITH`** (an older alias). Write it undirected, filter `WHERE n <> a`; works inside `[*1..N]` |
| `BGP_PATH` | BGP_PATH_OBSERVATION → ASN | An observed AS path traverses this network; the only route to `BGP_PATH_OBSERVATION` |
| `CONFLICTS_WITH` | ANNOUNCED_PREFIX → ASN | MOAS conflict (*computed*) |
| `ROA_AUTHORIZES_ORIGIN` | ROA → ASN | RPKI authorizes origin AS |
| `ROA_AUTHORIZES_PREFIX` | ROA → PREFIX | RPKI authorizes prefix |
| `OPERATES` | TLD_OPERATOR → TLD | Registry operator |

### WHOIS & registration

| Edge | From → To | Notes |
|------|-----------|-------|
| `HAS_REGISTRAR` | HOSTNAME → REGISTRAR | Current registrar |
| `PREV_REGISTRAR` | HOSTNAME → REGISTRAR | Historical — track transfers |
| `HAS_EMAIL` | HOSTNAME → EMAIL | WHOIS contact |
| `HAS_PHONE` | HOSTNAME → PHONE | WHOIS contact |
| `REGISTERED_BY` | HOSTNAME/ASN/REGISTERED_PREFIX → ORGANIZATION | Registrant / owning org |
| `SAME_ORG_AS` | ORGANIZATION → ORGANIZATION | Folds a raw registrant string to its canonical company |

### Geo

| Edge | From → To | Notes |
|------|-----------|-------|
| `LOCATED_IN` | IPV4/IPV6 → CITY | Chain `HAS_COUNTRY` for the country |
| `HAS_COUNTRY` | ANNOUNCED_PREFIX/ASN/CITY/IPV4/IPV6/ORGANIZATION/PHONE/PREFIX/REGISTERED_PREFIX → COUNTRY | Country code |

### Threat intel & egress

| Edge | From → To | Notes |
|------|-----------|-------|
| `LISTED_IN` | IPV4/IPV6/HOSTNAME → FEED_SOURCE | Carries `firstSeen`/`lastSeen`/`weight` (*computed*) |
| `TAGGED_AS` | IPV4/IPV6/HOSTNAME/ASN → THREAT_TAG | Malware/campaign family; the only route to `THREAT_TAG` |
| `HAS_SIGNAL` | IPV4/IPV6/HOSTNAME/ASN/PREFIX → THREAT_SIGNAL_TYPE | Curated infra signal — check which label carries the signal you want |
| `ATTRIBUTED_TO` | HOSTNAME/IPV4/PREFIX → ACTOR | Indicator attributed to a named adversary; read the caveat below |
| `OPERATES_EXIT_NODE` | IPV4 → TOR_RELAY | Tor-exit identity |
| `DELEGATED_TO` | PREFIX/IPV4/VENDOR → VENDOR | Cloud/SaaS operator (distinct from WHOIS owner) |
| `USES_TECHNIQUE` | ACTOR → ATTACK_PATTERN | The curated MITRE ATT&CK mapping — 9,256 edges. Not Whisper's own attribution |
| `USES_TACTIC` | ATTACK_PATTERN → ATTACK_PATTERN | A technique grouped under its tactic |

### Email security (SPF / DMARC / DKIM)

| Edge | From → To | Notes |
|------|-----------|-------|
| `SPF_INCLUDE` | HOSTNAME → HOSTNAME | `include:` (chainable) |
| `SPF_IP` | HOSTNAME → IPV4/IPV6/PREFIX | `ip4:`/`ip6:` |
| `SPF_A` / `SPF_MX` / `SPF_EXISTS` / `SPF_REDIRECT` | HOSTNAME → HOSTNAME | Other SPF mechanisms |
| `DMARC_REPORTS_TO` | HOSTNAME → DMARC_RECIPIENT | Where DMARC reports go |
| `DKIM_SIGNED_BY` | HOSTNAME → VENDOR | Mail vendor whose key signs the domain |
| `EMITS_TLS_FINGERPRINT` | IPV4 → TLS_FINGERPRINT | JA3/JARM |

### Physical infrastructure & CT

| Edge | From → To | Notes |
|------|-----------|-------|
| `AS_PRESENT_AT` | ASN → FACILITY | Network in a datacenter |
| `IX_MEMBER` | ASN → INTERNET_EXCHANGE | Network at an IXP |
| `IX_HOSTED_AT` | INTERNET_EXCHANGE → FACILITY | IXP's building |
| `CABLE_LANDS_AT` | SUBMARINE_CABLE → CABLE_LANDING | Subsea cable landing |
| `LANDING_NEAR` | CABLE_LANDING → FACILITY | Landing near a facility |
| `CDN_POP_AT` | CDN_POP → FACILITY | CDN PoP in a facility |
| `FIBER_SEGMENT` | FACILITY → FACILITY | Fiber link (traverse undirected for both ends) |
| `PREFIX_IN_REGION` | PREFIX → CLOUD_REGION | Prefix in a cloud region |
| `SEEN_IN_CT` | HOSTNAME → CT_OBSERVATION | Subdomain/SAN discovery (anchor the host) |

> **Computed edges:** `ROUTES`, `HAS_NAME`, `CONFLICTS_WITH`, `ANNOUNCED_BY`, `LISTED_IN`, `BGP_NEIGHBOR`, and `BELONGS_TO → CATEGORY` are synthesized at query time. They work inside a variable-length `[*1..N]` when one endpoint is anchored by name: keep the range tight, filter `WHERE n <> a` on peering walks, and for a wide fan-out write explicit single hops joined with `WITH ... LIMIT`. Read per-type counts from `CALL db.relationshipTypes() YIELD type, count`, never from `count(r)` over unanchored endpoints.

---

## Direction landmines (memorize these)

| Edge | Right way |
|------|-----------|
| `RESOLVES_TO` | `HOSTNAME → IPV4`. No PTR edge — reverse with `(ip)<-[:RESOLVES_TO]-(h)` |
| `MAIL_FOR` / `NAMESERVER_FOR` | server → domain. A domain's MX/NS: `(d)<-[:MAIL_FOR]-(mx)` |
| `CHILD_OF` | child → parent |
| `ANNOUNCED_BY` then `ROUTES` | IP → origin AS: `(ip)-[:ANNOUNCED_BY]->(ap)<-[:ROUTES]-(asn)`. Never join `ROUTES` and `BELONGS_TO` in one pattern |
| `BGP_NEIGHBOR` | symmetric in practice — `(asn)-[:BGP_NEIGHBOR]-(peer) WHERE peer <> asn` |
| `LINKS_TO` | URL → HOSTNAME; anchor the URL first |
| `LOCATED_IN` | IPV4 → CITY, then `CITY-[:HAS_COUNTRY]->COUNTRY`; an IP also carries `HAS_COUNTRY` directly |

---

## Must-know traversal chains

**Host → network owner (attribution)** — who hosts a domain and on whose AS (`ROUTES` matches in either direction):

```cypher expect=rows>0 seed=github.com verified=2026-09-02
MATCH (h:HOSTNAME {name:"github.com"})-[:RESOLVES_TO]->(ip:IPV4)
      -[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)-[:ROUTES]->(a:ASN)-[:HAS_NAME]->(n:ASN_NAME)
RETURN ip.name, ap.name, a.name AS asn, n.name AS network LIMIT 5
```

To bound each stage, put the routing leg in a `CALL { WITH ip ... }` subquery, or give each `WITH` stage one computed hop (`ANNOUNCED_BY`, then `ROUTES`). A walk this long needs an account, so [sign in](https://console.whisper.security/sign-in?redirect_url=https%3A%2F%2Fwww.whisper.security%2Fdocs%2Fcypher%2Fcheat-sheet) before you run it.

**IP → jurisdiction** — `(:IPV4)-[:LOCATED_IN]->(:CITY)-[:HAS_COUNTRY]->(:COUNTRY)`

**IP → origin AS** — `MATCH (ip:IPV4 {name:"1.1.1.1"})-[:ANNOUNCED_BY]->(ap:ANNOUNCED_PREFIX)<-[:ROUTES]-(a:ASN) RETURN ap.name, a.name LIMIT 5`

**Blast radius (one indicator → the campaign)** — pivot on shared infra:

```cypher expect=rows>0 seed=172.67.75.10 verified=2026-09-02
MATCH (ip:IPV4 {name:"172.67.75.10"})<-[:RESOLVES_TO]-(h:HOSTNAME)
WITH h LIMIT 25
OPTIONAL MATCH (h)-[:HAS_EMAIL]->(e:EMAIL)<-[:HAS_EMAIL]-(sibling:HOSTNAME)
RETURN h.name, e.name, collect(DISTINCT sibling.name)[..20] AS shared_registrant
```

Bound the co-tenant set with `WITH h LIMIT 25` before the registrant pivot. A trailing `LIMIT` will not do it: the sibling collect still runs against every co-tenant first, and the `[..20]` slice is applied only after the whole list is built.

**Domain → mail / name servers (reverse traversal)** — `MATCH (d:HOSTNAME {name:"github.com"})<-[:MAIL_FOR]-(mx) RETURN mx.name LIMIT 10`

**MOAS / possible BGP hijack** — `MATCH (p:ANNOUNCED_PREFIX {name:"216.168.228.0/24"})-[:CONFLICTS_WITH]->(a:ASN) RETURN p.name, collect(a.name) LIMIT 25`

**IP → feeds → categories** — `(:IPV4)-[:LISTED_IN]->(:FEED_SOURCE)-[:BELONGS_TO]->(:CATEGORY)`

**ASN → physical footprint** — `(:ASN)-[:AS_PRESENT_AT]->(:FACILITY)` and `(:ASN)-[:IX_MEMBER]->(:INTERNET_EXCHANGE)-[:IX_HOSTED_AT]->(:FACILITY)`

**ASN → RPKI** — `MATCH (a:ASN {name:"AS13335"})<-[:ROA_AUTHORIZES_ORIGIN]-(r:ROA) WHERE r.authSource = "rpki-roa" RETURN r.prefix, r.asn, r.maxLength LIMIT 10` (a `ROA` has no `name`)

**Unclassified token → typed entity** — `CALL whisper.search("1.1.1.1") YIELD kind, name, matchType RETURN kind, name, matchType LIMIT 5`

**Actor → ATT&CK** — `MATCH (a:ACTOR {name:"APT28"})-[:USES_TECHNIQUE]->(t:ATTACK_PATTERN) RETURN t.name LIMIT 25`

> That chain reads the curated MITRE ATT&CK knowledge base — 9,256 `USES_TECHNIQUE` edges and 872 `USES_TACTIC` edges across 1,944 actors and 712 techniques. **It is a reference layer, not Whisper's own attribution.** The edge that joins it to live infrastructure is sparse: `ATTRIBUTED_TO`, a published attribution from an indicator to a named group, holds 302 edges. The chain returns technique and tactic rollups. It does not attribute anything.

The [Workflows](/docs/workflows) have copy-paste recipes per workflow.

---

## Threat properties on a node

Threat-listed `IPV4` / `IPV6` / `HOSTNAME` nodes carry the verdict inline, so one anchored read gives the whole posture — no extra hops:

```cypher expect=rows>0 seed=185.220.101.1 verified=2026-09-02
MATCH (ip:IPV4 {name:"185.220.101.1"})
RETURN ip.threatScore, ip.threatLevel, ip.isThreat, ip.isTor, ip.isAnonymizer LIMIT 1
```

- `threatScore` (numeric), `threatLevel` (`NONE` … `CRITICAL`), and the flags `isThreat`, `isTor`, `isAnonymizer`.
- Name the properties you read. `RETURN ip` may leave the reconciled verdict fields out for speed and say so with a `projection-verdict-omitted` advisory; `projectionFull: true` on the request returns the full surface.
- For the scored reasoning — feeds, weights, factors — call `explain()`. A `NONE`/clean read means "not listed at this granularity," not "safe."

> **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.
> Full contract: [Coverage — what we looked at](/docs/whisper-graph/procedures/coverage).

---

## Procedures — one-liners

Call from Cypher with `CALL`. **Quote every argument**, and `YIELD` exact names: a column a procedure does not emit is rejected, not ignored. Full signatures and the complete census (52 procedures) are in [Procedures](/docs/whisper-graph/procedures).

| Procedure | Exact `YIELD` columns | Notes |
|-----------|-----------------------|-------|
| `explain("indicator")` | `indicator, type, found, score, level, explanation, factors, sources, coverage` | IP, host, ASN, CIDR, file hash or CVE id; read `coverage` before `level`. Multi-shape, so `YIELD *` is rejected; `sources[]` carry `feedId, weight, firstSeen, lastSeen` |
| `whisper.assess("host")` / `whisper.assess([hosts])` | `host, label, band, sub_labels, signals, coverage, evidence, verdictScore, isThreat, threatSources, company_apex, company_linkage, company_name, company_sector, company_hq_country, pl_generation` | Verdict **plus** `coverage` (what we looked at) and `evidence[]` (why). Single string or list; a URL folds to its host. The last six columns describe the company behind the host; without company data on your access, `company_name`, `company_sector` and `company_hq_country` come back empty |
| `whisper.assessUrl(urls)` | `url, host, path, apex_band, path_band, band, coverage, evidence` | Single string or list |
| `whisper.enrich([indicators])` | `name, owner, country, asn, band, prevalence, coverage, company_apex, company_linkage, company_name, company_sector, company_hq_country, pl_generation` | Rows are de-duplicated by name, not aligned to your input: join by `name`. `owner` is a network attribution |
| `whisper.identify("host")` | `host, vendor_id, canonical_name, category, confidence, roles, host_class, band, company_apex, company_linkage, company_name, company_sector, company_hq_country, pl_generation` | Whose infrastructure this is |
| `whisper.walk("host"[, depth, budget])` | `host, no_atlas_match, nearest_known_vendors, coverage` | Structural neighbourhood when `identify` has no direct match |
| `whisper.origins("domain"[, options])` | `ip, confidence, methods, asnName, kind, category, truncated` | Real origin IPs behind a CDN/proxy |
| `whisper.variants("domain")` | `variant, method, exists, nodeId, label, confidence, confidenceLabel` | `exists: true` means *registered*, not *malicious* |
| `whisper.resolve("host")` | `host, a, aaaa, coverage` | |
| `whisper.search("token"[, options])` | `query, kind, name, matchedField, matchType, warning` | Bounded lookup of an unclassified token; options `types`, `mode`, `suffix`, `limit` |
| `whisper.history.whois("domain")` | `indicator, registrableDomain, registrar, registrant, country, createDate, updateDate, expiryDate, nameServers` | Stable WHOIS shape; a subdomain folds to its apex with a `whois-parent-fold` advisory |
| `whisper.history.bgp("ip\|asn\|prefix")` | `indicator, type, origin, prefix, startTime, endTime, visibility, peersSeing, cached` | Stable routing shape (note the spelling `peersSeing`) |
| `whisper.history("indicator")` | one shape or the other | Multi-shape: `YIELD` within one shape, or call a single-shape variant above |
| `whisper.lookupTlsFingerprint("hash")` | `indicator, found, kind, hash, category, label, family, vendor, client, sourceCount, firstSeen, lastSeen` | `ja3:`/`jarm:` prefix optional; a hostname returns `found: false` |
| `whisper.lookupTorRelay("ip")` | `indicator, found, fingerprint, exitAddresses, exitAddressCount, exitAddressesV6, exitAddressCountV6, source, ingestedAt` | |
| `whisper.asnThreatDensity("AS13335")` | `asn, listedIps, announcedIpv4, routedPrefixes, densityRatio, coverage` | |
| `whisper.topAsnsByPrefixCount(n)` | `asn, prefixCount` | Integer argument |
| `whisper.psl.tldPlusOne("host")` / `whisper.psl.isPublicSuffix("name")` | `apex` / `result` | Registrable apex and public-suffix test |
| `db.labels()` / `db.relationshipTypes()` / `db.schema()` | `label` / `type, count, sourceLabels, targetLabels, …` / schema rows | Introspect the live schema before anchoring — cheap, and they answer immediately. The column is `type`, not `relationshipType` |

> `explain()` auto-detects the indicator type. `exists: true` from `whisper.variants()` means *registered*, not *malicious* — pivot the hit through `explain()` for a verdict. A successful response may also carry a top-level `advisories[]` array (`whois-parent-fold`, `enrich-semantics`, `null-pagination-param`, `projection-verdict-omitted`, …): read it rather than parsing rows.

---

## Query rules (one line each)

| Do this | Not that |
|---------|----------|
| Anchor on `name`: `MATCH (h:HOSTNAME {name:"example.com"})` | `MATCH (h:HOSTNAME) WHERE h.name CONTAINS "example"` |
| Lowercase the value in your code | `toLower()` around the anchor |
| Prefix match: `WHERE h.name STARTS WITH "mail."` | Regex: `WHERE h.name =~ "^mail\\..*"` |
| Suffix match: `WHERE h.name ENDS WITH ".example.com"` | Broad `ENDS WITH "example.com"` (scan) |
| `CALL whisper.search("token")` for an unclassified token | `CONTAINS` across an unanchored label |
| Always add `LIMIT`, including on `CALL ... YIELD ... RETURN` | Open-ended traversal on a billion-node label |
| `WITH x LIMIT n` before the fan-out, then `collect` | A trailing `LIMIT` after an unbounded expansion |
| `UNWIND [...] AS n MATCH (h:HOSTNAME {name: n})` | One request per indicator |
| `GET /api/query/stats` for global counts | `MATCH ()-[r]->() RETURN count(r)` |
| `OPTIONAL MATCH` for sparse WHOIS fields | Mandatory `MATCH` (drops rows silently) |
| `-[:BGP_NEIGHBOR]-(n) WHERE n <> a` | `PEERS_WITH` |
| `count(DISTINCT p)` across an announced-prefix chain | `RETURN DISTINCT` over the same chain |
| Confirm with `CALL db.labels()` before anchoring | Guessing a `Domain`/`fqdn` that doesn't exist |
| `CALL whisper.identify("host")` (quoted) | `CALL whisper.identify(host)` |
| `CALL explain(ip)` for scoring | Manual `ASN→PREFIX→IP→LISTED_IN` walks on a large network |

**Speed guide:** anchored point lookups = instant · `STARTS WITH` / narrow `ENDS WITH ".dom"` = fast · anchored multi-hop with `OPTIONAL MATCH` = seconds · unanchored label scans, unanchored `URL` expansion, and regex `=~` over `HOSTNAME` = avoid.

---

## Send one

Sign in to get a key, then send it in `X-API-Key`. [Getting Started](/docs/whisper-graph/getting-started) walks the setup.

```bash
curl -s -A "whisper-client/1.0" https://graph.whisper.security/api/query \
  -H "Content-Type: application/json" \
  -H "X-API-Key: $WHISPER_API_KEY" \
  -d '{"query":"MATCH (h:HOSTNAME {name:\"www.google.com\"})-[:RESOLVES_TO]->(ip) RETURN h.name, ip.name LIMIT 3"}'
```

See also the [Threat Feeds & Categories](/docs/whisper-graph/threat-feeds) reference (134 feeds / 32 categories) and the [HTTP API](/docs/cypher-api) for the full endpoint reference.
