# Errors

> WhisperGraph API errors: the RFC 7807 problem+json body, every type slug the engine returns and what to do about each, the suggestions array, what an unrecognised key really does, and how to report a problem.

*Source: https://www.whisper.security/docs/cypher-api/errors*

---
When a request to the Whisper API fails, the response body tells you what went wrong and, for query errors, usually how to fix it. This page covers that body, the `type` slugs the engine returns, and what to do about each one.

One thing that is not on this page: a query that succeeds but returns zero rows. That is almost always a wrong label or edge name, not an error. Check `CALL db.labels()` and `CALL db.relationshipTypes()` first, and see [Best Practices](/docs/cypher/best-practices).

## Error format

Errors come back as an RFC 7807 problem document, sent as `Content-Type: application/problem+json`: a `type` URI under `https://whisper.security/errors/`, a `title`, a `status`, a `detail`, an `instance` and a `timestamp`. A query error adds a `suggestions` array that proposes a rewrite, and some types add machine-readable fields of their own: `query-unservable` carries a `reason`, for instance.

**The `type` slug is the stable surface. Branch on it, and never on the prose in `detail`,** which is written for a human and changes.

An unbound variable, captured from the live API on 2026-09-02:

```json
{
  "type": "https://whisper.security/errors/query-error",
  "title": "Query Error",
  "status": 400,
  "detail": "Variable 'nosuchvar' is not defined in this scope. Bind it with MATCH (nosuchvar:LABEL …), WITH … AS nosuchvar, UNWIND … AS nosuchvar, or YIELD nosuchvar.",
  "instance": "/api/query",
  "timestamp": "2026-09-02T16:14:54.587509203Z",
  "suggestions": [
    {
      "kind": "undefined_variable",
      "rationale": "The query references 'nosuchvar', which was never bound by a preceding MATCH / WITH / UNWIND / YIELD in scope. Bind it first, or reference a variable that is in scope.",
      "rewrite": "Introduce every RETURN/WHERE variable with a preceding MATCH (var:LABEL ...) / WITH var / UNWIND ... AS var / YIELD var.",
      "confidence": "high",
      "safeToAutoRetry": false
    }
  ]
}
```

Each `suggestions` entry carries a `kind`, a `rationale`, a `rewrite`, a `confidence`, and `safeToAutoRetry` — the field an automated client should read before re-running anything on your behalf. `suggestions` is sometimes present and empty; treat that the same as absent.

## Status codes and `type` slugs

| Status | `type` | Condition | What to do |
|--------|--------|-----------|------------|
| `400` | `query-error` | The Cypher is malformed, names an unbound variable or an unsupplied `$parameter`, or calls a procedure the engine does not have. Also what a write clause returns. | Fix the query. `detail` names the position, the variable or the procedure, and `suggestions` proposes a rewrite. See [Syntax & Clauses](/docs/cypher/syntax). |
| `400` | `query-validation-error` | The request carried no statement — an empty `query` field, or a body with no `query` in it. | Send a `query` field with a statement in it. |
| `400` | `query-unservable` | The engine cannot plan the shape. `reason` says which: `global_edge_count` (an edge count with both endpoints bare) or `unanchored_virtual_edge_scan` (a computed edge with neither endpoint labelled or anchored). | Label or anchor at least one endpoint of a computed edge. For a per-type edge total, `CALL db.relationshipTypes() YIELD type, count` is precomputed and instant; for the global total, read [GET /api/query/stats](/docs/cypher-api/reference/stats). |
| `400` | `query-depth-exceeded` | The pattern is deeper than your access allows. | [Sign in](https://console.whisper.security/sign-in?redirect_url=https%3A%2F%2Fwww.whisper.security%2Fdocs%2Fcypher-api), or decompose the chain: run the first leg, anchor the next request on what came back, or let a [procedure](/docs/whisper-graph/procedures) such as `whisper.enrich()` do the join in one call. [Best Practices](/docs/cypher/best-practices#the-performance-habits) shows how to stage a traversal. |
| `400` | `missing-query-parameter` | The `GET` or form-encoded form was called with no `q`. | Send the query as `?q=` or a form field named `q`, or use `POST` with a JSON body. |
| `400` | `malformed-request-body` | The request body is not valid JSON. | Check the body. Request fields are listed in the [API Reference](/docs/cypher-api/reference). |
| `403` | *(not a problem document)* | A browser page sent a cross-origin request; the API does not answer those. | Call the API from a server or a script, and put your own backend between a browser front end and the API. |
| `408` | `query-timeout` | The query ran past its time budget. The body adds `timeoutMs` and `elapsedMs`. | Narrow it: anchor on a `name`, bound the wide hop with `WITH ... LIMIT`, and stage the traversal. |
| `415` | `unsupported-media-type` | The `Content-Type` is neither JSON nor form-encoded. | Send `Content-Type: application/json` on `POST`. |
| `429` | `query-quota-exceeded` | You sent more requests at once, or more in a row, than your access allows right now. | Wait, then retry. Put a list of indicators into one `UNWIND` query instead of one request each. |

Every slug above is stable; key your handling on the slug and read `detail` for the specifics. The API is read-only: write clauses (`CREATE`, `MERGE`, `SET`, `DELETE`, `REMOVE`, `DROP`) are refused as a `query-error` before anything runs, with a `readonly_engine` suggestion whose `safeToAutoRetry` is `false`. Nothing you send can modify the graph. For any status not in this table, retry once, then [report it](#reporting-issues) with the request id.

### Confirm the key was accepted

A missing, mistyped or unrecognised API key does not fail the request. The query runs with reduced access and answers `200`, so the symptom of a bad key is never an authentication error: it is a query that is refused, or a result thinner than the one you expected. Before you debug the query, run `CALL whisper.quota()` and check that the `isAnonymous` row is `false`. If it is `true`, re-check the header format on the [HTTP API](/docs/cypher-api) page.

The MCP connector behaves differently: it rejects an unrecognised key outright with `401`. A working MCP session therefore says nothing about the header your HTTP client sends, and the reverse; confirm each surface on its own.

## When a backing service is briefly unavailable

`explain()` and `whisper.history()` read from a live threat-intelligence backend at call time. If that backend is briefly unreachable, the procedure returns `available: false` with a `retryAfter` value instead of a score; the HTTP request itself still succeeds. Respect the retry interval and call again. Batching indicators with `UNWIND ... CALL explain(...)` makes one backend call per item, so runtime grows with the list; keep unwound lists short.

## Reporting issues

Two response headers make a request findable after the fact: `X-Request-Id` identifies the request, and `X-Served-By` identifies where it was answered. Both are present on every response, signed in and signed out. Quote both when you report a problem; the request id is how support finds your request.

When opening a support ticket, include:

1. The full request URL and request body
2. The full response, headers and body
3. The `X-Request-Id` and `X-Served-By` headers from the response
4. The time the request was made (UTC)

Start at [Support](/docs/support), or email [support@whisper.security](mailto:support@whisper.security).
