# POST /api/query

> POST /api/query reference: headers, the JSON body, the response envelope, parameter binding, batch statements and form bodies, in five languages.

*Source: https://www.whisper.security/docs/cypher-api/reference/query-post*

---
`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](/docs/cypher-api/reference) index.

## Request headers

| Header | Required | Notes |
|--------|----------|-------|
| `Content-Type: application/json` | yes | The body is JSON. `application/x-www-form-urlencoded` is also accepted; see [Form-encoded body](#form-encoded-body). |
| `X-API-Key` | no | Your key. `Authorization: Bearer <key>` / `ApiKey <key>` are also accepted. Without one the query runs with reduced access. |
| `User-Agent` | recommended | Send a descriptive value that names your client. |
| `X-Whisper-Client`, `X-Whisper-Client-Version` | no | Name and version of your integration, so support can tell your traffic apart. |
| `Idempotency-Key` | no | Read only by `CALL whisper.watch` when it creates a watch; see [Watches](/docs/guides/watches-and-alerting). |

## Request body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `query` (alias `q`) | string | yes | The Cypher to run. Statements separated by a top-level `;` run as a batch. |
| `parameters` | object | no | Named values for `$param` placeholders in the query. The field is `parameters`, not `params`. |
| `timeout` | number | no | Milliseconds to allow for this query. A value above what your access allows is lowered, not honored. |
| `projectionFull` | boolean | no | Default `false`: a whole-node projection (`RETURN n`, `keys(n)`, `properties(n)`) omits the reconciled threat-verdict properties and the response carries a `projection-verdict-omitted` advisory. Set `true` to include them. |

## Call it

```whisper-code-tabs
{
  "curl": "curl -s -A \"whisper-client/1.0\" \\\n  -X POST https://graph.whisper.security/api/query \\\n  -H \"Content-Type: application/json\" \\\n  -H \"X-API-Key: $WHISPER_API_KEY\" \\\n  -d '{\"query\": \"MATCH (h:HOSTNAME {name: \\\"google.com\\\"})-[:RESOLVES_TO]->(ip:IPV4) RETURN ip.name AS ip LIMIT 5\"}'",
  "python": "import requests\n\nres = requests.post(\n    \"https://graph.whisper.security/api/query\",\n    headers={\n        \"Content-Type\": \"application/json\",\n        \"X-API-Key\": \"whisper-YOUR_API_KEY\",\n        \"User-Agent\": \"whisper-client/1.0\",\n    },\n    json={\n        \"query\": \"MATCH (h:HOSTNAME {name: $name})-[:RESOLVES_TO]->(ip:IPV4) RETURN ip.name AS ip LIMIT 5\",\n        \"parameters\": {\"name\": \"google.com\"},\n    },\n)\ndata = res.json()\nprint(data[\"rows\"])",
  "node": "const res = await fetch(\"https://graph.whisper.security/api/query\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    \"X-API-Key\": process.env.WHISPER_API_KEY,\n    \"User-Agent\": \"whisper-client/1.0\",\n  },\n  body: JSON.stringify({\n    query:\n      \"MATCH (h:HOSTNAME {name: $name})-[:RESOLVES_TO]->(ip:IPV4) RETURN ip.name AS ip LIMIT 5\",\n    parameters: { name: \"google.com\" },\n  }),\n});\nconst data = await res.json();\nconsole.log(data.rows);",
  "go": "package main\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\nfunc main() {\n\tbody := []byte(`{\"query\":\"MATCH (h:HOSTNAME {name: $name})-[:RESOLVES_TO]->(ip:IPV4) RETURN ip.name AS ip LIMIT 5\",\"parameters\":{\"name\":\"google.com\"}}`)\n\treq, _ := http.NewRequest(\"POST\", \"https://graph.whisper.security/api/query\", bytes.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"X-API-Key\", \"whisper-YOUR_API_KEY\")\n\treq.Header.Set(\"User-Agent\", \"whisper-client/1.0\")\n\tres, _ := http.DefaultClient.Do(req)\n\tdefer res.Body.Close()\n\tout, _ := io.ReadAll(res.Body)\n\tfmt.Println(string(out))\n}",
  "ruby": "require \"net/http\"\nrequire \"json\"\nrequire \"uri\"\n\nuri = URI(\"https://graph.whisper.security/api/query\")\nreq = Net::HTTP::Post.new(uri, {\n  \"Content-Type\" => \"application/json\",\n  \"X-API-Key\" => \"whisper-YOUR_API_KEY\",\n  \"User-Agent\" => \"whisper-client/1.0\",\n})\nreq.body = {\n  query: \"MATCH (h:HOSTNAME {name: $name})-[:RESOLVES_TO]->(ip:IPV4) RETURN ip.name AS ip LIMIT 5\",\n  parameters: { name: \"google.com\" },\n}.to_json\n\nres = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }\nputs JSON.parse(res.body)[\"rows\"]"
}
```

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}
}
```

On a cache hit, `statistics` also carries `cached: true` and `cachedExecutionTimeMs`. A response may add `advisories` or `rewrittenQuery`; the [API Reference](/docs/cypher-api/reference#response-envelope) describes both.

## 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 expect=skip reason="request document, not a standalone query — $name is supplied by the sibling parameters object, so the query string alone does not run"
{
  "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` and a `missing_parameter` suggestion — the placeholder is never treated as a literal.

## Batch statements

Statements separated by a top-level `;` in one `query` string run as a batch, in order. The response is then a `results` array with one element per statement instead of the single envelope:

```json
{
  "results": [
    {
      "result": {"columns": ["a"], "rows": [{"a": 1}], "rowCount": 1, "executionTimeMs": 0, "cacheHit": false},
      "outcome": "OK",
      "success": true
    },
    {
      "outcome": "PARSE_ERROR",
      "errorMessage": "Expected ')' but got 'RETURN' at position 18",
      "errorType": "CypherParseException",
      "success": false
    }
  ]
}
```

`outcome` is one of `OK`, `PARSE_ERROR`, `EXECUTION_ERROR` or `DEADLINE_EXCEEDED`. A statement that succeeds carries its `result`; one that fails carries `errorMessage` and `errorType` instead, and the batch as a whole still answers `200`. Check each element's `success`, not the HTTP status. Batching is for the JSON body only; the `GET` and form-encoded variants run a single statement.

## Form-encoded body

The same endpoint accepts `Content-Type: application/x-www-form-urlencoded`. Send the query as `q`, with optional `timeout` and `projectionFull` fields. There is no `parameters` field in this form; use the JSON body when you need bindings.

```bash
curl -s -A "whisper-client/1.0" \
  -X POST https://graph.whisper.security/api/query \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "X-API-Key: $WHISPER_API_KEY" \
  --data-urlencode "q=RETURN 1 AS ok"
```

## Errors

A failed request answers with an RFC 7807 problem document instead of the envelope above. [Errors](/docs/cypher-api/errors) describes that body, lists every `type` slug and status this endpoint returns, and says what to do about each.

For the `GET` variant and the stats endpoint, see [GET /api/query](/docs/cypher-api/reference/query-get) and [GET /api/query/stats](/docs/cypher-api/reference/stats).
