Skip to contentSkip navigation

POST /api/query

The main query endpoint. Send read-only Cypher as JSON, bind parameters, and run batch statements. Request and response reference with code in five languages.

API Reference
On this page (6)

POST /api/query Documentation

POST /api/query is the main query endpoint. Send a read-only Cypher query as JSON and you get back columns and rows. It is the endpoint every runnable example in these docs goes through, and the one to use for anything real — it carries parameters cleanly, has no URL length limit, and supports batch statements.

Base URL: https://graph.whisper.security. Authentication is shared across the API and covered on the API Reference index.

Request headers

HeaderRequiredNotes
Content-Type: application/jsonyesThe body is JSON.
X-API-KeynoYour key. Authorization: Bearer <key> / ApiKey <key> are also accepted.
User-AgentrecommendedSend an explicit value to avoid a WAF 403.

Request body

FieldTypeRequiredDescription
query (or q)stringyesThe Cypher to run. ;-separated statements run as a batch.
parametersobjectnoNamed values for $param placeholders in the query.
timeoutnumbernoHow long to let this query run, in milliseconds.

Call it

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

A successful response is the standard envelope:

json
{
  "columns": ["ip"],
  "rows": [
    {"ip": "142.250.154.100"},
    {"ip": "142.250.154.139"},
    {"ip": "142.250.191.14"},
    {"ip": "142.251.110.100"},
    {"ip": "142.251.110.102"}
  ],
  "statistics": {"rowCount": 5, "executionTimeMs": 3}
}

Parameter binding

The request field is named parameters (not params). Use $name placeholders in the query and pass the values as an object. Parameters keep the query plan cacheable and avoid escaping headaches with quotes inside JSON:

json
{
  "query": "MATCH (h:HOSTNAME {name: $name})-[:RESOLVES_TO]->(ip:IPV4) RETURN ip.name AS ip LIMIT 5",
  "parameters": {"name": "google.com"}
}

Send the query string without its parameters object and the API answers 400 query-error with Missing parameter: $name — the placeholder is never treated as a literal.

Batch statements

;-separated statements in one query string run as a batch. The response is then a results array with one element per statement, each carrying its own result, outcome, and success fields.

Errors

Error bodies are RFC 7807-shaped, but they are sent as Content-Type: application/json, not application/problem+json — branch on the body's type, never on the content-type header. Measured 2026-08-09 against production:

json
{
  "type": "https://whisper.security/errors/query-error",
  "title": "Query Error",
  "status": 400,
  "detail": "Expected ')' but got 'RETURN' at position 38",
  "timestamp": "2026-08-09T19:15:47.003487950Z",
  "suggestions": [
    {
      "kind": "balance_punctuation",
      "rationale": "Unmatched parentheses or brackets in the query.",
      "rewrite": "Count and balance (), [], {} pairs. Common cause: typo in node/relationship pattern.",
      "confidence": "medium",
      "safeToAutoRetry": false
    }
  ]
}

The type slug is the stable surface — key your handling on it, not on the prose in title or detail. For the GET variant and the stats endpoint, see GET /api/query and GET /api/query/stats.