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.
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
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | yes | The body is JSON. |
X-API-Key | no | Your key. Authorization: Bearer <key> / ApiKey <key> are also accepted. |
User-Agent | recommended | Send an explicit value to avoid a WAF 403. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
query (or q) | string | yes | The Cypher to run. ;-separated statements run as a batch. |
parameters | object | no | Named values for $param placeholders in the query. |
timeout | number | no | How 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:
{
"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:
{
"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:
{
"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.