# The Request Envelope

> The whisper.agents wire format: the POST body, the map literal syntax, and the op, ok, status, result and error columns every call returns.

*Source: https://www.whisper.security/docs/control-plane/envelope*

---
Every control call is one HTTP request carrying one Cypher statement, and every answer arrives in the same envelope.

## The endpoint

`POST https://graph.whisper.online/api/query`, with the statement in a JSON field named `query`. `GET` and `HEAD` answer a read the same way.

| Header | Value |
|---|---|
| `X-API-Key` | `<your key>`, of the form `whisper_live_...` |
| `content-type` | `application/json` |

```bash
curl -s https://graph.whisper.online/api/query \
  -H 'X-API-Key: whisper_live_...' \
  -H 'content-type: application/json' \
  --data-binary @- <<'JSON'
{"query": "CALL whisper.agents({op: 'agent', args: {agent: 'checkout-bot'}})"}
JSON
```

A form-encoded body works as well. Send the same statement as a field named `q`.

```bash
curl -s https://graph.whisper.online/api/query \
  -H 'X-API-Key: whisper_live_...' \
  --data-urlencode "q=CALL whisper.agents({op: 'list', args: {kind: 'agents'}})"
```

## Writing the map literal {#map-literal}

`whisper.agents` takes a single map. `op` names the operation and `args` carries that operation's arguments.

- Keys are bare. A quoted key is rejected.
- String values are single-quoted. To put a `'` inside a string, double it.
- Numbers and booleans are written bare, and arrays in square brackets.
- If you generate the call rather than type it, sort the keys, so the same arguments always produce the same statement.

```whisper-call
CALL whisper.agents({op: 'policy', args: {allow: ['api.example.com'], default: 'deny'}})
```

## What comes back

One row, with these columns.

| Column | Holds |
|---|---|
| `op` | The operation you named. |
| `ok` | `true` when the operation ran. |
| `status` | The status of the operation itself. |
| `result` | The operation's own payload, as `{columns, rows}`. |
| `error` | Absent on success. On failure a string, or an object with `code`, `message` and `retryable`. |
| `retry_after` | Seconds to wait, on the statuses that carry it. |
| `elapsed_ms` | How long the operation took. |

The outer object has the same `{columns, rows}` shape as any Cypher answer, and its rows are objects keyed by column name. Inside `result`, rows are positional arrays that line up with `result.columns`.

```text
{
  "columns": ["op", "ok", "status", "result", "error", "retry_after", "elapsed_ms"],
  "rows": [
    {
      "op": "list",
      "ok": true,
      "status": 200,
      "result": {
        "columns": ["kind", "item"],
        "rows": [
          ["agents", {"label": "checkout-bot", "address": "<the agent's /128>", "state": "<state>"}]
        ]
      },
      "error": null,
      "retry_after": null,
      "elapsed_ms": <ms>
    }
  ]
}
```

Read `result.columns` for the field order, then walk `result.rows` positionally. Treat `ok: false` as a failed operation rather than a failed request, and read `error` for the reason.

## Raw Cypher on the same endpoint

The endpoint serves `whisper.agents` itself and forwards any other read to the graph with your key. Graph statements are read-only here: a write clause comes back `400 read_only`. Query syntax and the graph's own error format are in the [HTTP API](/docs/cypher-api) chapter.

## When a call fails

A failed operation keeps the envelope and fills `error`; [Error codes](/docs/control-plane/errors) lists each code with its status. A statement the parser cannot read is a different thing, and comes back as a problem document instead, described in [Errors](/docs/cypher-api/errors).
